@agentix-e/nl2spel 1.2.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.
Files changed (76) hide show
  1. package/README.md +3 -1
  2. package/dist/index.cjs +3062 -0
  3. package/dist/index.d.cts +917 -0
  4. package/dist/index.d.ts +917 -28
  5. package/dist/index.js +3021 -24
  6. package/package.json +18 -7
  7. package/dist/context/context-extractor.d.ts +0 -86
  8. package/dist/context/context-extractor.d.ts.map +0 -1
  9. package/dist/context/context-extractor.js +0 -181
  10. package/dist/context/context-extractor.js.map +0 -1
  11. package/dist/context/schema-formatter.d.ts +0 -20
  12. package/dist/context/schema-formatter.d.ts.map +0 -1
  13. package/dist/context/schema-formatter.js +0 -104
  14. package/dist/context/schema-formatter.js.map +0 -1
  15. package/dist/engine/nl2spel-engine.d.ts +0 -145
  16. package/dist/engine/nl2spel-engine.d.ts.map +0 -1
  17. package/dist/engine/nl2spel-engine.js +0 -130
  18. package/dist/engine/nl2spel-engine.js.map +0 -1
  19. package/dist/index.d.ts.map +0 -1
  20. package/dist/index.js.map +0 -1
  21. package/dist/pattern/builtin-patterns.d.ts +0 -12
  22. package/dist/pattern/builtin-patterns.d.ts.map +0 -1
  23. package/dist/pattern/builtin-patterns.js +0 -812
  24. package/dist/pattern/builtin-patterns.js.map +0 -1
  25. package/dist/pattern/pattern-definition.d.ts +0 -49
  26. package/dist/pattern/pattern-definition.d.ts.map +0 -1
  27. package/dist/pattern/pattern-definition.js +0 -2
  28. package/dist/pattern/pattern-definition.js.map +0 -1
  29. package/dist/pattern/pattern-matcher.d.ts +0 -55
  30. package/dist/pattern/pattern-matcher.d.ts.map +0 -1
  31. package/dist/pattern/pattern-matcher.js +0 -216
  32. package/dist/pattern/pattern-matcher.js.map +0 -1
  33. package/dist/provider/llm-provider.d.ts +0 -146
  34. package/dist/provider/llm-provider.d.ts.map +0 -1
  35. package/dist/provider/llm-provider.js +0 -2
  36. package/dist/provider/llm-provider.js.map +0 -1
  37. package/dist/provider/provider-registry.d.ts +0 -28
  38. package/dist/provider/provider-registry.d.ts.map +0 -1
  39. package/dist/provider/provider-registry.js +0 -65
  40. package/dist/provider/provider-registry.js.map +0 -1
  41. package/dist/strategy/strategy-router.d.ts +0 -79
  42. package/dist/strategy/strategy-router.d.ts.map +0 -1
  43. package/dist/strategy/strategy-router.js +0 -182
  44. package/dist/strategy/strategy-router.js.map +0 -1
  45. package/dist/template/intent-classifier.d.ts +0 -56
  46. package/dist/template/intent-classifier.d.ts.map +0 -1
  47. package/dist/template/intent-classifier.js +0 -251
  48. package/dist/template/intent-classifier.js.map +0 -1
  49. package/dist/template/nl-intent.d.ts +0 -38
  50. package/dist/template/nl-intent.d.ts.map +0 -1
  51. package/dist/template/nl-intent.js +0 -39
  52. package/dist/template/nl-intent.js.map +0 -1
  53. package/dist/template/prompts/prompt-builder.d.ts +0 -20
  54. package/dist/template/prompts/prompt-builder.d.ts.map +0 -1
  55. package/dist/template/prompts/prompt-builder.js +0 -243
  56. package/dist/template/prompts/prompt-builder.js.map +0 -1
  57. package/dist/template/template-engine.d.ts +0 -27
  58. package/dist/template/template-engine.d.ts.map +0 -1
  59. package/dist/template/template-engine.js +0 -378
  60. package/dist/template/template-engine.js.map +0 -1
  61. package/dist/utils/chinese-number-parser.d.ts +0 -30
  62. package/dist/utils/chinese-number-parser.d.ts.map +0 -1
  63. package/dist/utils/chinese-number-parser.js +0 -92
  64. package/dist/utils/chinese-number-parser.js.map +0 -1
  65. package/dist/validation/auto-fixer.d.ts +0 -13
  66. package/dist/validation/auto-fixer.d.ts.map +0 -1
  67. package/dist/validation/auto-fixer.js +0 -110
  68. package/dist/validation/auto-fixer.js.map +0 -1
  69. package/dist/validation/self-correction-loop.d.ts +0 -46
  70. package/dist/validation/self-correction-loop.d.ts.map +0 -1
  71. package/dist/validation/self-correction-loop.js +0 -132
  72. package/dist/validation/self-correction-loop.js.map +0 -1
  73. package/dist/validation/validation-pipeline.d.ts +0 -81
  74. package/dist/validation/validation-pipeline.d.ts.map +0 -1
  75. package/dist/validation/validation-pipeline.js +0 -306
  76. package/dist/validation/validation-pipeline.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,24 +1,3021 @@
1
- // Core public API — @agentix-e/nl2spel
2
- // ProviderRegistry
3
- export { ProviderRegistry } from './provider/provider-registry.js';
4
- // Context
5
- export { ContextExtractor } from './context/context-extractor.js';
6
- export { SchemaFormatter } from './context/schema-formatter.js';
7
- export { PatternMatcher } from './pattern/pattern-matcher.js';
8
- export { BUILTIN_PATTERNS } from './pattern/builtin-patterns.js';
9
- // Template (Layer 1)
10
- export { NLIntent } from './template/nl-intent.js';
11
- export { IntentClassifier } from './template/intent-classifier.js';
12
- export { TemplateEngine } from './template/template-engine.js';
13
- export { PromptBuilder } from './template/prompts/prompt-builder.js';
14
- // Validation (Layer 2)
15
- export { ValidationPipeline } from './validation/validation-pipeline.js';
16
- export { AutoFixer } from './validation/auto-fixer.js';
17
- export { SelfCorrectionLoop } from './validation/self-correction-loop.js';
18
- // Strategy Router (Layer 2)
19
- export { StrategyRouter } from './strategy/strategy-router.js';
20
- // NL2Spel Engine
21
- export { NL2SpelEngine } from './engine/nl2spel-engine.js';
22
- // Utils
23
- export { ChineseNumberParser } from './utils/chinese-number-parser.js';
24
- //# sourceMappingURL=index.js.map
1
+ // src/provider/provider-registry.ts
2
+ var ProviderRegistry = class {
3
+ _providers = [];
4
+ _nextIndex = 0;
5
+ /**
6
+ * Register a Provider.
7
+ * @param provider LLMProvider instance
8
+ * @param options.priority User-assigned priority (lower = preferred). Defaults to registration order.
9
+ */
10
+ register(provider, options) {
11
+ if (this._providers.some((p) => p.provider.name === provider.name)) {
12
+ throw new Error(`Provider '${provider.name}' already registered`);
13
+ }
14
+ this._providers.push({
15
+ provider,
16
+ priority: options?.priority ?? this._nextIndex,
17
+ index: this._nextIndex
18
+ });
19
+ this._nextIndex++;
20
+ }
21
+ /** Unregister a Provider */
22
+ unregister(name) {
23
+ this._providers = this._providers.filter((p) => p.provider.name !== name);
24
+ }
25
+ /** Get a Provider by name */
26
+ get(name) {
27
+ return this._providers.find((p) => p.provider.name === name)?.provider;
28
+ }
29
+ /**
30
+ * Get available Providers sorted by priority.
31
+ * Sort rule: offline first → user priority (asc) → registration order (asc)
32
+ */
33
+ async getPrioritized() {
34
+ const available = [];
35
+ for (const entry of this._providers) {
36
+ if (await entry.provider.isAvailable()) {
37
+ available.push(entry);
38
+ }
39
+ }
40
+ return available.sort((a, b) => {
41
+ const aOffline = a.provider.capabilities.offlineAvailable;
42
+ const bOffline = b.provider.capabilities.offlineAvailable;
43
+ if (aOffline && !bOffline) return -1;
44
+ if (!aOffline && bOffline) return 1;
45
+ if (a.priority !== b.priority) return a.priority - b.priority;
46
+ return a.index - b.index;
47
+ }).map((entry) => entry.provider);
48
+ }
49
+ /**
50
+ * Explicitly reorder providers by name.
51
+ * Providers not listed retain their position after the reordered ones.
52
+ */
53
+ reorder(providerNames) {
54
+ const orderMap = new Map(providerNames.map((name, i) => [name, i]));
55
+ const maxExisting = this._providers.reduce(
56
+ (max, p) => Math.max(max, p.priority),
57
+ providerNames.length - 1
58
+ );
59
+ for (const entry of this._providers) {
60
+ const explicitIndex = orderMap.get(entry.provider.name);
61
+ entry.priority = explicitIndex ?? maxExisting + entry.index + 1;
62
+ }
63
+ }
64
+ /** List all registered Providers */
65
+ list() {
66
+ return this._providers.map((p) => p.provider);
67
+ }
68
+ /** Number of registered Providers */
69
+ get count() {
70
+ return this._providers.length;
71
+ }
72
+ };
73
+
74
+ // src/context/context-extractor.ts
75
+ var ContextExtractor = class {
76
+ /**
77
+ * Extract ContextSchema from plain data structures (no spel-ts dependency)
78
+ */
79
+ extract(context) {
80
+ const schema = {
81
+ root: this.extractRoot(context),
82
+ variables: this.extractVariables(context),
83
+ beans: this.extractBeans(context),
84
+ types: this.extractTypes(context),
85
+ functions: this.extractFunctions(context)
86
+ };
87
+ return schema;
88
+ }
89
+ /**
90
+ * Extract root object schema
91
+ */
92
+ extractRoot(context) {
93
+ const { rootObject, rootName } = context;
94
+ if (rootObject === null || rootObject === void 0) return null;
95
+ const name = rootName ?? "root";
96
+ const type = typeof rootObject === "object" ? rootObject?.constructor?.name ?? "Object" : typeof rootObject;
97
+ const fields = this.extractFields(rootObject);
98
+ return { name, type, fields, methods: {} };
99
+ }
100
+ /**
101
+ * Extract object fields
102
+ */
103
+ /**
104
+ * Recursively extract fields from an object, following nested objects
105
+ * all the way to leaf values. Uses a visited set to prevent circular references.
106
+ */
107
+ extractFields(obj, visited = /* @__PURE__ */ new WeakSet()) {
108
+ if (obj === null || obj === void 0 || typeof obj !== "object") {
109
+ return {};
110
+ }
111
+ if (visited.has(obj)) return {};
112
+ visited.add(obj);
113
+ const fields = {};
114
+ for (const key of Object.keys(obj)) {
115
+ try {
116
+ const value = obj[key];
117
+ const fieldType = this.inferSpelType(value);
118
+ const isArray = Array.isArray(value);
119
+ const isObject = !isArray && typeof value === "object" && value !== null && !(value instanceof Date);
120
+ const field = {
121
+ type: fieldType,
122
+ description: this.generateDescription(key, fieldType),
123
+ isCollection: isArray,
124
+ elementType: isArray && value.length > 0 ? typeof value[0] : void 0,
125
+ nullable: value === null,
126
+ example: this.safeExample(value)
127
+ };
128
+ if (isObject) {
129
+ field.fields = this.extractFields(value, visited);
130
+ }
131
+ fields[key] = field;
132
+ } catch {
133
+ }
134
+ }
135
+ return fields;
136
+ }
137
+ /**
138
+ * Infer SpEL type
139
+ */
140
+ inferSpelType(value) {
141
+ if (value === null || value === void 0) return "string";
142
+ if (typeof value === "string") return "string";
143
+ if (typeof value === "number") return "number";
144
+ if (typeof value === "boolean") return "boolean";
145
+ if (value instanceof Date) return "date";
146
+ if (Array.isArray(value)) return "array";
147
+ if (typeof value === "object") return "object";
148
+ return "string";
149
+ }
150
+ /**
151
+ * Generate field description
152
+ */
153
+ generateDescription(key, type) {
154
+ return `Field '${key}' of type ${type}`;
155
+ }
156
+ /**
157
+ * Safely get example value
158
+ */
159
+ safeExample(value) {
160
+ if (value === null) return null;
161
+ if (typeof value === "object") return void 0;
162
+ return value;
163
+ }
164
+ /**
165
+ * Extract variables
166
+ */
167
+ extractVariables(context) {
168
+ const variables = {};
169
+ if (!context.variables) return variables;
170
+ for (const [name, vdef] of Object.entries(context.variables)) {
171
+ variables[name] = {
172
+ type: vdef.type,
173
+ description: vdef.description ?? `Variable '#${name}'`,
174
+ nullable: vdef.nullable ?? false,
175
+ value: vdef.value
176
+ };
177
+ }
178
+ return variables;
179
+ }
180
+ /**
181
+ * Extract beans
182
+ */
183
+ extractBeans(context) {
184
+ const beans = {};
185
+ if (!context.beans) return beans;
186
+ for (const bdef of context.beans) {
187
+ beans[bdef.name] = {
188
+ type: bdef.type,
189
+ description: bdef.description,
190
+ singleton: bdef.singleton
191
+ };
192
+ }
193
+ return beans;
194
+ }
195
+ /**
196
+ * Extract types
197
+ */
198
+ extractTypes(context) {
199
+ const types = {};
200
+ if (!context.types) return types;
201
+ for (const tdef of context.types) {
202
+ types[tdef.name] = {
203
+ className: tdef.className,
204
+ description: tdef.description
205
+ };
206
+ }
207
+ return types;
208
+ }
209
+ /**
210
+ * Extract functions
211
+ */
212
+ extractFunctions(context) {
213
+ const functions = {};
214
+ if (!context.functions) return functions;
215
+ for (const fdef of context.functions) {
216
+ functions[fdef.name] = {
217
+ returnType: fdef.returnType,
218
+ params: fdef.params,
219
+ description: fdef.description
220
+ };
221
+ }
222
+ return functions;
223
+ }
224
+ };
225
+
226
+ // src/context/schema-formatter.ts
227
+ var SchemaFormatter = class {
228
+ /**
229
+ * LLM-optimized format — compact, injected into the User part of the prompt
230
+ */
231
+ formatForLLM(schema) {
232
+ const lines = ["## Context Schema"];
233
+ if (schema.root) {
234
+ lines.push("");
235
+ lines.push(`Root Object (accessible as #${schema.root.name}):`);
236
+ lines.push(` Type: ${schema.root.type}`);
237
+ lines.push(` Fields:`);
238
+ for (const [name, field] of Object.entries(schema.root.fields)) {
239
+ lines.push(this.formatFieldForLLM(name, field, " "));
240
+ }
241
+ }
242
+ if (Object.keys(schema.variables).length > 0) {
243
+ lines.push("");
244
+ lines.push(`Variables:`);
245
+ for (const [name, varDef] of Object.entries(schema.variables)) {
246
+ lines.push(
247
+ ` #${name}: ${varDef.type}` + (varDef.description ? ` (${varDef.description})` : "")
248
+ );
249
+ }
250
+ }
251
+ if (Object.keys(schema.beans).length > 0) {
252
+ lines.push("");
253
+ lines.push(`Beans:`);
254
+ for (const [name, beanDef] of Object.entries(schema.beans)) {
255
+ lines.push(
256
+ ` @${name}: ${beanDef.type}` + (beanDef.description ? ` (${beanDef.description})` : "")
257
+ );
258
+ }
259
+ }
260
+ if (Object.keys(schema.types).length > 0) {
261
+ lines.push("");
262
+ lines.push(`Types (accessible via T(typeName)):`);
263
+ for (const [name, typeDef] of Object.entries(schema.types)) {
264
+ lines.push(` ${name}` + (typeDef.description ? `: ${typeDef.description}` : ""));
265
+ }
266
+ }
267
+ if (Object.keys(schema.functions).length > 0) {
268
+ lines.push("");
269
+ lines.push(`Functions:`);
270
+ for (const [name, funcDef] of Object.entries(schema.functions)) {
271
+ const params = funcDef.params.map((p) => `${p.name}: ${p.type}`).join(", ");
272
+ lines.push(` #${name}(${params}): ${funcDef.returnType}`);
273
+ }
274
+ }
275
+ lines.push("");
276
+ return lines.join("\n");
277
+ }
278
+ /**
279
+ * Human-readable format
280
+ */
281
+ formatForHuman(schema) {
282
+ const lines = [
283
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
284
+ " Context Schema (Human-Readable)",
285
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"
286
+ ];
287
+ if (schema.root) {
288
+ lines.push("");
289
+ lines.push(`\u250C\u2500 Root: #${schema.root.name}`);
290
+ lines.push(`\u2502 Type: ${schema.root.type}`);
291
+ lines.push(`\u2502 Fields:`);
292
+ const entries = Object.entries(schema.root.fields);
293
+ for (let i = 0; i < entries.length; i++) {
294
+ const [name, field] = entries[i];
295
+ const prefix = i < entries.length - 1 ? "\u251C\u2500" : "\u2514\u2500";
296
+ lines.push(
297
+ `\u2502 ${prefix} ${name} (${field.type})` + (field.description ? ` \u2014 ${field.description}` : "") + (field.nullable ? " | nullable" : "") + (field.isCollection ? ` | collection<${field.elementType ?? "unknown"}>` : "")
298
+ );
299
+ }
300
+ lines.push(`\u2502 \u2514\u2500 (${entries.length} fields total)`);
301
+ }
302
+ return lines.join("\n");
303
+ }
304
+ formatFieldForLLM(name, field, indent) {
305
+ const parts = [`${indent}- ${name}: ${field.type}`];
306
+ if (field.description) {
307
+ parts.push(`(${field.description})`);
308
+ }
309
+ if (field.isCollection) {
310
+ parts.push(`[array of ${field.elementType ?? "unknown"}]`);
311
+ }
312
+ if (field.nullable) {
313
+ parts.push(`[nullable]`);
314
+ }
315
+ if (field.example !== void 0) {
316
+ parts.push(`(e.g. ${JSON.stringify(field.example)})`);
317
+ }
318
+ return parts.join(" ");
319
+ }
320
+ };
321
+
322
+ // src/utils/chinese-number-parser.ts
323
+ var ChineseNumberParser = class {
324
+ static DIGITS = {
325
+ \u96F6: 0,
326
+ \u4E00: 1,
327
+ \u4E8C: 2,
328
+ \u4E09: 3,
329
+ \u56DB: 4,
330
+ \u4E94: 5,
331
+ \u516D: 6,
332
+ \u4E03: 7,
333
+ \u516B: 8,
334
+ \u4E5D: 9,
335
+ \u4E24: 2
336
+ };
337
+ static UNITS = {
338
+ \u5341: 10,
339
+ \u767E: 100,
340
+ \u5343: 1e3,
341
+ \u4E07: 1e4,
342
+ \u4EBF: 1e8
343
+ };
344
+ static CHINESE_NUM_PATTERN = /^[零一二三四五六七八九十百千万亿两]+$/;
345
+ /**
346
+ * Check if a string is pure Chinese numbers
347
+ */
348
+ static isChineseNumber(text) {
349
+ return this.CHINESE_NUM_PATTERN.test(text);
350
+ }
351
+ /**
352
+ * Parse Chinese numbers → Arabic numerals
353
+ *
354
+ * @param text Chinese number string
355
+ * @returns Parsed number, returns NaN on failure
356
+ */
357
+ static parse(text) {
358
+ if (!this.isChineseNumber(text)) {
359
+ return NaN;
360
+ }
361
+ let s = text;
362
+ if (s.startsWith("\u5341")) {
363
+ s = "\u4E00" + s;
364
+ }
365
+ let result = 0;
366
+ let section = 0;
367
+ let current = 0;
368
+ for (const ch of s) {
369
+ const digit = this.DIGITS[ch];
370
+ if (digit !== void 0) {
371
+ current = digit;
372
+ } else {
373
+ const unit = this.UNITS[ch];
374
+ if (unit === void 0) continue;
375
+ if (unit >= 1e4) {
376
+ result += (section + current || 1) * unit;
377
+ section = 0;
378
+ current = 0;
379
+ } else {
380
+ section += (current || 1) * unit;
381
+ current = 0;
382
+ }
383
+ }
384
+ }
385
+ return result + section + current;
386
+ }
387
+ /**
388
+ * Safe parse: returns null if parsing fails
389
+ */
390
+ static parseSafe(text) {
391
+ const value = this.parse(text);
392
+ return Number.isNaN(value) ? null : value;
393
+ }
394
+ };
395
+
396
+ // src/pattern/pattern-matcher.ts
397
+ var PatternMatcher = class {
398
+ _patterns;
399
+ constructor(patterns = []) {
400
+ this._patterns = [...patterns];
401
+ this.sortByPriority();
402
+ }
403
+ get patternCount() {
404
+ return this._patterns.length;
405
+ }
406
+ register(pattern) {
407
+ this._patterns.push(pattern);
408
+ this.sortByPriority();
409
+ }
410
+ registerAll(patterns) {
411
+ this._patterns.push(...patterns);
412
+ this.sortByPriority();
413
+ }
414
+ sortByPriority() {
415
+ this._patterns.sort((a, b) => b.priority - a.priority);
416
+ }
417
+ /**
418
+ * Match natural language input, return the best match
419
+ */
420
+ match(nl) {
421
+ const startTime = Date.now();
422
+ const normalized = this.normalize(nl);
423
+ for (const pattern of this._patterns) {
424
+ const matchResult = pattern.match.exec(normalized);
425
+ if (!matchResult) continue;
426
+ const slots = {};
427
+ if (matchResult.groups) {
428
+ for (const [key, value] of Object.entries(matchResult.groups)) {
429
+ if (value !== void 0) {
430
+ slots[key] = value;
431
+ }
432
+ }
433
+ }
434
+ const spel = this.fillTemplate(pattern, slots, normalized, matchResult);
435
+ const latencyMs2 = Date.now() - startTime;
436
+ return { matched: true, pattern, spel, confidence: pattern.confidence, latencyMs: latencyMs2, slots };
437
+ }
438
+ const latencyMs = Date.now() - startTime;
439
+ return { matched: false, confidence: 0, latencyMs };
440
+ }
441
+ /**
442
+ * Batch match
443
+ */
444
+ matchAll(nl, maxResults = 5) {
445
+ const normalized = this.normalize(nl);
446
+ const results = [];
447
+ for (const pattern of this._patterns) {
448
+ if (results.length >= maxResults) break;
449
+ const matchResult = pattern.match.exec(normalized);
450
+ if (!matchResult) continue;
451
+ const slots = {};
452
+ if (matchResult.groups) {
453
+ for (const [key, value] of Object.entries(matchResult.groups)) {
454
+ if (value !== void 0) slots[key] = value;
455
+ }
456
+ }
457
+ results.push({
458
+ matched: true,
459
+ pattern,
460
+ spel: this.fillTemplate(pattern, slots, normalized, matchResult),
461
+ confidence: pattern.confidence,
462
+ latencyMs: 0,
463
+ slots
464
+ });
465
+ }
466
+ return results;
467
+ }
468
+ /**
469
+ * Input normalization
470
+ */
471
+ normalize(input) {
472
+ return input.trim().replace(/[\uFF01-\uFF5E]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/\s+/g, " ").replace(/[,,。.!!??;;::]+$/, "");
473
+ }
474
+ /**
475
+ * Infer SpEL root object name from Chinese input
476
+ */
477
+ inferRoot(input) {
478
+ if (/^(?:订单|order)/i.test(input)) return "order";
479
+ if (/^(?:用户|user)/i.test(input)) return "user";
480
+ if (/^(?:文件|file)/i.test(input)) return "file";
481
+ if (/^(?:账号|account)/i.test(input)) return "account";
482
+ if (/^(?:商品|product|item)/i.test(input)) return "product";
483
+ return "order";
484
+ }
485
+ /**
486
+ * Extract Chinese field names from input and map to SpEL fields
487
+ */
488
+ extractChineseField(input) {
489
+ const m = input.match(/^[^\s,,、]+/);
490
+ if (!m) return "value";
491
+ const first = m[0];
492
+ const cnMap = {
493
+ \u5907\u6CE8: "remark",
494
+ \u8BF4\u660E: "description",
495
+ \u63CF\u8FF0: "description",
496
+ \u91D1\u989D: "amount",
497
+ \u6570\u91CF: "count",
498
+ \u4E2A\u6570: "count",
499
+ \u72B6\u6001: "status",
500
+ \u7C7B\u578B: "type",
501
+ \u540D\u79F0: "name",
502
+ \u6807\u9898: "title",
503
+ \u5730\u5740: "address",
504
+ \u90AE\u7BB1: "email",
505
+ \u624B\u673A: "phone",
506
+ \u7535\u8BDD: "phone",
507
+ \u65E5\u671F: "date",
508
+ \u65F6\u95F4: "time",
509
+ \u5E74\u9F84: "age",
510
+ \u4EF7\u683C: "price",
511
+ \u7528\u6237\u540D: "name",
512
+ \u6743\u9650: "role",
513
+ \u6807\u7B7E: "tags",
514
+ \u5217\u8868: "list",
515
+ \u6570\u7EC4: "items",
516
+ \u6587\u4EF6: "file",
517
+ \u6587\u4EF6\u540D: "name",
518
+ \u8FC7\u671F: "expiryDate",
519
+ \u521B\u5EFA: "createdAt",
520
+ \u6709\u6548: "valid",
521
+ \u6D3B\u8DC3: "active",
522
+ \u6FC0\u6D3B: "active"
523
+ };
524
+ return cnMap[first] ?? first;
525
+ }
526
+ /**
527
+ * Template filling and value transformation
528
+ */
529
+ fillTemplate(pattern, slots, originalInput, _matchResult) {
530
+ let result = pattern.spelTemplate;
531
+ const hasFieldSlot = "field" in slots;
532
+ const field = hasFieldSlot ? this.inferFieldFromCapture(slots["field"]) : this.extractChineseField(originalInput);
533
+ const root = this.inferRoot(originalInput);
534
+ result = result.replace(/\{field\}/g, field);
535
+ result = result.replace(/\{root\}/g, root);
536
+ if (originalInput.includes(">=") || originalInput.includes("\u2265")) {
537
+ result = result.replace(/\{op\}/g, ">=");
538
+ } else if (originalInput.includes("<=") || originalInput.includes("\u2264")) {
539
+ result = result.replace(/\{op\}/g, "<=");
540
+ } else if (originalInput.includes("!=") || originalInput.includes("\u2260")) {
541
+ result = result.replace(/\{op\}/g, "!=");
542
+ } else if (originalInput.includes(">") || originalInput.includes("\u5927\u4E8E") || originalInput.includes("\u8D85\u8FC7")) {
543
+ result = result.replace(/\{op\}/g, ">");
544
+ } else if (originalInput.includes("<") || originalInput.includes("\u5C0F\u4E8E") || originalInput.includes("\u4F4E\u4E8E")) {
545
+ result = result.replace(/\{op\}/g, "<");
546
+ } else {
547
+ result = result.replace(/\{op\}/g, ">");
548
+ }
549
+ for (const [key, value] of Object.entries(slots)) {
550
+ if (key === "field") continue;
551
+ const def = pattern.slots[key];
552
+ let transformedValue = value;
553
+ if (def?.transform === "toNumber") {
554
+ const cnNumber = ChineseNumberParser.parseSafe(value);
555
+ transformedValue = cnNumber !== null ? String(cnNumber) : value;
556
+ }
557
+ result = result.replace(`{${key}}`, transformedValue ?? "");
558
+ }
559
+ result = result.replace(/\{[a-zA-Z_]+\}/g, "");
560
+ return result.trim();
561
+ }
562
+ /**
563
+ * Infer SpEL field name from capture group
564
+ */
565
+ inferFieldFromCapture(captured) {
566
+ if (/^[a-zA-Z_]\w*$/.test(captured)) return captured;
567
+ return this.extractChineseField(captured);
568
+ }
569
+ };
570
+
571
+ // src/pattern/builtin-patterns.ts
572
+ var BUILTIN_PATTERNS = [
573
+ // ================================================================
574
+ // P93+: Pure English numeric comparison (covers "amount > 500" etc.)
575
+ // ================================================================
576
+ {
577
+ id: "EN-CMP-GT",
578
+ match: /^(?<field>\w+)\s*>\s*(?<value>\d+(?:\.\d+)?)/,
579
+ spelTemplate: "#{field} > {value}",
580
+ slots: { value: { key: "value", type: "number", transform: "toNumber" } },
581
+ priority: 93,
582
+ tags: ["comparison", "gt", "english"],
583
+ examples: [{ nl: "amount > 500", spel: "#amount > 500" }],
584
+ difficulty: "easy",
585
+ confidence: 0.98
586
+ },
587
+ {
588
+ id: "EN-CMP-LT",
589
+ match: /^(?<field>\w+)\s*(?:less\s+than|lower\s+than|<)\s*(?<value>\d+(?:\.\d+)?)/i,
590
+ spelTemplate: "#{field} < {value}",
591
+ slots: { value: { key: "value", type: "number", transform: "toNumber" } },
592
+ priority: 94,
593
+ tags: ["comparison", "lt", "english"],
594
+ examples: [
595
+ { nl: "amount less than 100", spel: "#amount < 100" },
596
+ { nl: "price lower than 50", spel: "#price < 50" }
597
+ ],
598
+ difficulty: "easy",
599
+ confidence: 0.98
600
+ },
601
+ {
602
+ id: "EN-CMP-GE",
603
+ match: /^(?<field>\w+)\s*>=\s*(?<value>\d+(?:\.\d+)?)/,
604
+ spelTemplate: "#{field} >= {value}",
605
+ slots: { value: { key: "value", type: "number", transform: "toNumber" } },
606
+ priority: 93,
607
+ tags: ["comparison", "ge", "english"],
608
+ examples: [{ nl: "amount >= 50", spel: "#amount >= 50" }],
609
+ difficulty: "easy",
610
+ confidence: 0.98
611
+ },
612
+ {
613
+ id: "EN-CMP-LE",
614
+ match: /^(?<field>\w+)\s*<=\s*(?<value>\d+(?:\.\d+)?)/,
615
+ spelTemplate: "#{field} <= {value}",
616
+ slots: { value: { key: "value", type: "number", transform: "toNumber" } },
617
+ priority: 93,
618
+ tags: ["comparison", "le", "english"],
619
+ examples: [{ nl: "amount <= 200", spel: "#amount <= 200" }],
620
+ difficulty: "easy",
621
+ confidence: 0.98
622
+ },
623
+ // ================================================================
624
+ // Group 1: Null / Empty — pri 97-96 (highest specificity)
625
+ // ================================================================
626
+ {
627
+ id: "CN-NULL-NOT",
628
+ match: /^(?<field>\S+?)\s*(?:不为空|不是空的|有值|存在|不为null)$/,
629
+ spelTemplate: "#{field} != null",
630
+ slots: {},
631
+ priority: 98,
632
+ tags: ["null", "isNotNull"],
633
+ examples: [{ nl: "\u5907\u6CE8\u4E0D\u4E3A\u7A7A", spel: "#\u5907\u6CE8 != null" }],
634
+ difficulty: "easy",
635
+ confidence: 0.98
636
+ },
637
+ {
638
+ id: "CN-NULL-IS",
639
+ match: /^(?<field>\S+?)\s*(?:为空|是空的|不存在|无值|为null)$/,
640
+ spelTemplate: "#{field} == null",
641
+ slots: {},
642
+ priority: 97,
643
+ tags: ["null", "isNull"],
644
+ examples: [{ nl: "\u5907\u6CE8\u4E3A\u7A7A", spel: "#\u5907\u6CE8 == null" }],
645
+ difficulty: "easy",
646
+ confidence: 0.98
647
+ },
648
+ {
649
+ id: "EN-NULL-IS",
650
+ match: /^(?<field>\w+)\s+is\s+(?:null|empty)$/i,
651
+ spelTemplate: "#{field} == null",
652
+ slots: {},
653
+ priority: 96,
654
+ tags: ["null", "isNull", "english"],
655
+ examples: [{ nl: "remark is null", spel: "#remark == null" }],
656
+ difficulty: "easy",
657
+ confidence: 0.98
658
+ },
659
+ {
660
+ id: "EN-NULL-NOT",
661
+ match: /^(?<field>\w+)\s+is\s+not\s+(?:null|empty)$/i,
662
+ spelTemplate: "#{field} != null",
663
+ slots: {},
664
+ priority: 96,
665
+ tags: ["null", "isNotNull", "english"],
666
+ examples: [{ nl: "remark is not null", spel: "#remark != null" }],
667
+ difficulty: "easy",
668
+ confidence: 0.98
669
+ },
670
+ // ================================================================
671
+ // Group 2: Chinese numeric comparison — pri 93
672
+ // ================================================================
673
+ {
674
+ id: "CN-CMP-GE",
675
+ match: /^(?<field>[^\s,,、]+?)\s*(?:金额|值)?\s*(?:不小于|不低于|大于等于|>=)\s*(?<value>\d+(?:\.\d+)?)/,
676
+ spelTemplate: "#{field} >= {value}",
677
+ slots: { value: { key: "value", type: "number", transform: "toNumber" } },
678
+ priority: 93,
679
+ tags: ["comparison", "ge", "chinese"],
680
+ examples: [{ nl: "\u91D1\u989D\u4E0D\u5C0F\u4E8E100", spel: "#\u91D1\u989D >= 100" }],
681
+ difficulty: "easy",
682
+ confidence: 0.95
683
+ },
684
+ {
685
+ id: "CN-CMP-LE",
686
+ match: /^(?<field>[^\s,,、]+?)\s*(?:金额|值)?\s*(?:不大于|不超过|小于等于|<=)\s*(?<value>\d+(?:\.\d+)?)/,
687
+ spelTemplate: "#{field} <= {value}",
688
+ slots: { value: { key: "value", type: "number", transform: "toNumber" } },
689
+ priority: 93,
690
+ tags: ["comparison", "le", "chinese"],
691
+ examples: [{ nl: "\u91D1\u989D\u4E0D\u8D85\u8FC7500", spel: "#\u91D1\u989D <= 500" }],
692
+ difficulty: "easy",
693
+ confidence: 0.95
694
+ },
695
+ {
696
+ id: "CN-CMP-GT",
697
+ match: /^(?<field>[^\s,,、]+?)\s*(?:金额|值|数量|价格)?\s*(?:大于|超过|高于)\s*(?<value>\d+(?:\.\d+)?)/,
698
+ spelTemplate: "#{field} > {value}",
699
+ slots: { value: { key: "value", type: "number", transform: "toNumber" } },
700
+ priority: 92,
701
+ tags: ["comparison", "gt", "chinese"],
702
+ examples: [{ nl: "\u8BA2\u5355\u91D1\u989D\u5927\u4E8E1000", spel: "#\u8BA2\u5355\u91D1\u989D > 1000" }],
703
+ difficulty: "easy",
704
+ confidence: 0.95
705
+ },
706
+ {
707
+ id: "CN-CMP-LT",
708
+ match: /^(?<field>[^\s,,、]+?)\s*(?:金额|值|数量|价格)?\s*(?:小于|低于|不到)\s*(?<value>\d+(?:\.\d+)?)/,
709
+ spelTemplate: "#{field} < {value}",
710
+ slots: { value: { key: "value", type: "number", transform: "toNumber" } },
711
+ priority: 92,
712
+ tags: ["comparison", "lt", "chinese"],
713
+ examples: [{ nl: "\u8BA2\u5355\u91D1\u989D\u5C0F\u4E8E500", spel: "#\u8BA2\u5355\u91D1\u989D < 500" }],
714
+ difficulty: "easy",
715
+ confidence: 0.95
716
+ },
717
+ // ================================================================
718
+ // Group 3: String/status equality — pri 91
719
+ // ================================================================
720
+ {
721
+ id: "CN-EQ-STATUS",
722
+ match: /^(?<field>[^\s,,、]+?)\s*(?:状态|类型)?\s*(?:等于|是|为)\s*(?<value>[^\s,,、]+)$/,
723
+ spelTemplate: "#{field} == '{value}'",
724
+ slots: { value: { key: "value", type: "string" } },
725
+ priority: 85,
726
+ tags: ["comparison", "eq", "string"],
727
+ examples: [{ nl: "\u8BA2\u5355\u72B6\u6001\u662F\u5DF2\u53D1\u8D27", spel: "#\u8BA2\u5355\u72B6\u6001 == '\u5DF2\u53D1\u8D27'" }],
728
+ difficulty: "easy",
729
+ confidence: 0.95
730
+ },
731
+ {
732
+ id: "EN-EQ-STRING",
733
+ match: /\b(?<field>\w+)\s+(?:equals?|==)\s+(?<value>[a-zA-Z_]\w*)/i,
734
+ spelTemplate: "#{field} == '{value}'",
735
+ slots: { value: { key: "value", type: "string" } },
736
+ priority: 91,
737
+ tags: ["comparison", "eq", "string", "english"],
738
+ examples: [{ nl: "status equals completed", spel: "#status == 'completed'" }],
739
+ difficulty: "easy",
740
+ confidence: 0.95
741
+ },
742
+ // CN: "order status is not cancelled" — must be before NOT pattern
743
+ {
744
+ id: "CN-NE-STATUS",
745
+ match: /^(?<field>[^\s,,、]+?)\s*(?:状态|类型)?\s*(?:不等于|!=|不是)\s*(?<value>[^\s,,、]+)$/,
746
+ spelTemplate: "#{field} != '{value}'",
747
+ slots: { value: { key: "value", type: "string" } },
748
+ priority: 91,
749
+ tags: ["comparison", "ne", "string"],
750
+ examples: [{ nl: "\u8BA2\u5355\u72B6\u6001\u4E0D\u662F\u5DF2\u53D6\u6D88", spel: "#\u8BA2\u5355\u72B6\u6001 != '\u5DF2\u53D6\u6D88'" }],
751
+ difficulty: "easy",
752
+ confidence: 0.95
753
+ },
754
+ {
755
+ id: "EN-NE-STRING",
756
+ match: /\b(?<field>\w+)\s*!=\s*(?<value>[a-zA-Z_]\w*)/i,
757
+ spelTemplate: "#{field} != '{value}'",
758
+ slots: { value: { key: "value", type: "string" } },
759
+ priority: 91,
760
+ tags: ["comparison", "ne", "string", "english"],
761
+ examples: [{ nl: "status != cancelled", spel: "#status != 'cancelled'" }],
762
+ difficulty: "easy",
763
+ confidence: 0.95
764
+ },
765
+ // CN/EN: count equality
766
+ {
767
+ id: "CN-EQ-COUNT",
768
+ match: /^(?<field>[^\s,,、]+?)\s*(?:数量|个数|计数)?\s*(?:等于|==)\s*(?<value>\d+)/,
769
+ spelTemplate: "#{field} == {value}",
770
+ slots: { value: { key: "value", type: "number", transform: "toNumber" } },
771
+ priority: 91,
772
+ tags: ["comparison", "eq", "number"],
773
+ examples: [{ nl: "\u6570\u91CF\u7B49\u4E8E5", spel: "#\u6570\u91CF == 5" }],
774
+ difficulty: "easy",
775
+ confidence: 0.95
776
+ },
777
+ {
778
+ id: "EN-EQ-NUMBER",
779
+ match: /\b(?<field>\w+)\s+(?:equals?|==)\s+(?<value>\d+)/i,
780
+ spelTemplate: "#{field} == {value}",
781
+ slots: { value: { key: "value", type: "number", transform: "toNumber" } },
782
+ priority: 91,
783
+ tags: ["comparison", "eq", "number", "english"],
784
+ examples: [{ nl: "count equals 10", spel: "#count == 10" }],
785
+ difficulty: "easy",
786
+ confidence: 0.95
787
+ },
788
+ // ================================================================
789
+ // Group 4: Permission checks — pri 88
790
+ // ================================================================
791
+ {
792
+ id: "CN-PERM-ROLE",
793
+ match: /^(?<field>[^\s,,、]+?)\s*(?:是|拥有|有)\s*(?<role>[^\s,,、]+?)(?:角色|权限)?$/,
794
+ spelTemplate: "hasRole('{role}')",
795
+ slots: { role: { key: "role", type: "string" } },
796
+ priority: 86,
797
+ tags: ["permission", "hasRole"],
798
+ examples: [{ nl: "\u7528\u6237\u662F\u7BA1\u7406\u5458", spel: "hasRole('\u7BA1\u7406\u5458')" }],
799
+ difficulty: "easy",
800
+ confidence: 0.9
801
+ },
802
+ {
803
+ id: "EN-PERM-ROLE",
804
+ match: /\buser\s+(?:has|is)\s+(?<role>[a-zA-Z_]+)\s*(?:role)?$/i,
805
+ spelTemplate: "hasRole('{role}')",
806
+ slots: { role: { key: "role", type: "string" } },
807
+ priority: 88,
808
+ tags: ["permission", "hasRole", "english"],
809
+ examples: [{ nl: "user has admin role", spel: "hasRole('admin')" }],
810
+ difficulty: "easy",
811
+ confidence: 0.9
812
+ },
813
+ {
814
+ id: "CN-PERM-PERM",
815
+ match: /^(?<field>[^\s,,、]+)\s*(?:可以|能够|允许|may|can)\s+(?<permission>.+)$/i,
816
+ spelTemplate: "hasPermission('{permission}')",
817
+ slots: { permission: { key: "permission", type: "string" } },
818
+ priority: 88,
819
+ tags: ["permission", "hasPermission"],
820
+ examples: [{ nl: "\u7528\u6237\u53EF\u4EE5\u5220\u9664\u8BA2\u5355", spel: "hasPermission('\u5220\u9664\u8BA2\u5355')" }],
821
+ difficulty: "easy",
822
+ confidence: 0.9
823
+ },
824
+ {
825
+ id: "EN-PERM-PERM",
826
+ match: /\buser\s+(?:can|may|is\s+allowed\s+to)\s+(?<permission>\w+)/i,
827
+ spelTemplate: "hasPermission('{permission}')",
828
+ slots: { permission: { key: "permission", type: "string" } },
829
+ priority: 88,
830
+ tags: ["permission", "hasPermission", "english"],
831
+ examples: [{ nl: "user can delete", spel: "hasPermission('delete')" }],
832
+ difficulty: "easy",
833
+ confidence: 0.9
834
+ },
835
+ // ================================================================
836
+ // Group 5: Collection operations — pri 85-89
837
+ // ================================================================
838
+ {
839
+ id: "CN-COLL-EMPTY",
840
+ match: /^(?<field>[^\s,,、]+?)\s*(?:列表|数组|集合)?\s*(?:为空|是空的|没有|无)\s*(?:元素|数据|项)?$/,
841
+ spelTemplate: "#{field}.isEmpty()",
842
+ slots: {},
843
+ priority: 89,
844
+ tags: ["collection", "isEmpty"],
845
+ examples: [{ nl: "\u8BA2\u5355\u5217\u8868\u4E3A\u7A7A", spel: "#\u8BA2\u5355\u5217\u8868.isEmpty()" }],
846
+ difficulty: "easy",
847
+ confidence: 0.98
848
+ },
849
+ {
850
+ id: "EN-COLL-EMPTY",
851
+ match: /^(?<field>\w+)\s+is\s+empty$/i,
852
+ spelTemplate: "#{field}.isEmpty()",
853
+ slots: {},
854
+ priority: 88,
855
+ tags: ["collection", "isEmpty", "english"],
856
+ examples: [{ nl: "items is empty", spel: "#items.isEmpty()" }],
857
+ difficulty: "easy",
858
+ confidence: 0.98
859
+ },
860
+ {
861
+ id: "CN-COLL-NOTEMPTY",
862
+ match: /^(?<field>[^\s,,、]+?)\s*(?:列表|数组|集合)?\s*(?:不为空|有)\s*(?:元素|数据|项)?$/,
863
+ spelTemplate: "!#{field}.isEmpty()",
864
+ slots: {},
865
+ priority: 89,
866
+ tags: ["collection", "isNotEmpty"],
867
+ examples: [{ nl: "\u8BA2\u5355\u5217\u8868\u6709\u6570\u636E", spel: "!#\u8BA2\u5355\u5217\u8868.isEmpty()" }],
868
+ difficulty: "easy",
869
+ confidence: 0.95
870
+ },
871
+ {
872
+ id: "EN-COLL-NOTEMPTY",
873
+ match: /^(?<field>\w+)\s+(?:has\s+items|is\s+not\s+empty)$/i,
874
+ spelTemplate: "!#{field}.isEmpty()",
875
+ slots: {},
876
+ priority: 86,
877
+ tags: ["collection", "isNotEmpty", "english"],
878
+ examples: [{ nl: "items is not empty", spel: "!#items.isEmpty()" }],
879
+ difficulty: "easy",
880
+ confidence: 0.95
881
+ },
882
+ {
883
+ id: "CN-COLL-CONTAINS",
884
+ match: /^(?<field>[^\s,,、]+?)\s*(?:列表|数组|集合)?\s*(?:中\s*)?(?:包含|含有|有)\s*(?<element>[^\s,,、]+)/,
885
+ spelTemplate: "#{field}.contains('{element}')",
886
+ slots: { element: { key: "element", type: "string" } },
887
+ priority: 87,
888
+ tags: ["collection", "contains"],
889
+ examples: [{ nl: "\u6807\u7B7E\u5217\u8868\u4E2D\u5305\u542BVIP", spel: "#\u6807\u7B7E\u5217\u8868.contains('VIP')" }],
890
+ difficulty: "easy",
891
+ confidence: 0.95
892
+ },
893
+ {
894
+ id: "EN-COLL-CONTAINS",
895
+ match: /\b(?<field>\w+)\s+(?:contains?|includes?)\s+(?<element>\S+)/i,
896
+ spelTemplate: "#{field}.contains('{element}')",
897
+ slots: { element: { key: "element", type: "string" } },
898
+ priority: 86,
899
+ tags: ["collection", "contains", "english"],
900
+ examples: [{ nl: "tags contains premium", spel: "#tags.contains('premium')" }],
901
+ difficulty: "easy",
902
+ confidence: 0.95
903
+ },
904
+ {
905
+ id: "CN-COLL-SIZE",
906
+ match: /^(?<field>[^\s,,、]+?)\s*(?:列表|数组|集合)?\s*(?:数量|个数|大小|长度)\s*(?<op>大于|>|超过|小于|<|等于|==)\s*(?<value>\d+)/,
907
+ spelTemplate: "#{field}.size() {op} {value}",
908
+ slots: {
909
+ value: { key: "value", type: "number", transform: "toNumber" },
910
+ op: { key: "op", type: "literal" }
911
+ },
912
+ priority: 85,
913
+ tags: ["collection", "size"],
914
+ examples: [{ nl: "\u8BA2\u5355\u5217\u8868\u6570\u91CF\u5927\u4E8E10", spel: "#\u8BA2\u5355\u5217\u8868.size() > 10" }],
915
+ difficulty: "medium",
916
+ confidence: 0.85
917
+ },
918
+ {
919
+ id: "EN-COLL-SIZE",
920
+ match: /\b(?<field>\w+)\s+(?:size|length|count)\s*(?<op>>|<|>=|<=|==)\s*(?<value>\d+)/i,
921
+ spelTemplate: "#{field}.size() {op} {value}",
922
+ slots: {
923
+ value: { key: "value", type: "number", transform: "toNumber" },
924
+ op: { key: "op", type: "literal" }
925
+ },
926
+ priority: 84,
927
+ tags: ["collection", "size", "english"],
928
+ examples: [{ nl: "items size > 5", spel: "#items.size() > 5" }],
929
+ difficulty: "medium",
930
+ confidence: 0.9
931
+ },
932
+ // ================================================================
933
+ // Group 6: String operations — pri 82-85
934
+ // ================================================================
935
+ {
936
+ id: "CN-STR-CONTAINS",
937
+ match: /^(?<field>[^\s,,、]+?)(?:备注|名称|描述|标签|标题)?\s*(?:包含|含有|包括)\s*(?<substr>[^\s,,、]+)/,
938
+ spelTemplate: "#{field}.contains('{substr}')",
939
+ slots: { substr: { key: "substr", type: "string" } },
940
+ priority: 85,
941
+ tags: ["string", "contains"],
942
+ examples: [{ nl: "\u8BA2\u5355\u5907\u6CE8\u5305\u542B\u52A0\u6025", spel: "#\u8BA2\u5355\u5907\u6CE8.contains('\u52A0\u6025')" }],
943
+ difficulty: "easy",
944
+ confidence: 0.95
945
+ },
946
+ {
947
+ id: "EN-STR-CONTAINS",
948
+ match: /\b(?<field>\w+)\s+(?:contains?|includes?)\s+(?<substr>\S+)/i,
949
+ spelTemplate: "#{field}.contains('{substr}')",
950
+ slots: { substr: { key: "substr", type: "string" } },
951
+ priority: 84,
952
+ tags: ["string", "contains", "english"],
953
+ examples: [{ nl: "name contains test", spel: "#name.contains('test')" }],
954
+ difficulty: "easy",
955
+ confidence: 0.95
956
+ },
957
+ {
958
+ id: "CN-STR-STARTS",
959
+ match: /^(?<field>[^\s,,、]+?)\s*以\s*(?<prefix>[^\s,,、]+)\s*(?:开头|开始)/,
960
+ spelTemplate: "#{field}.startsWith('{prefix}')",
961
+ slots: { prefix: { key: "prefix", type: "string" } },
962
+ priority: 85,
963
+ tags: ["string", "startsWith"],
964
+ examples: [{ nl: "\u8BA2\u5355\u53F7\u4EE5ORD\u5F00\u5934", spel: "#\u8BA2\u5355\u53F7.startsWith('ORD')" }],
965
+ difficulty: "easy",
966
+ confidence: 0.95
967
+ },
968
+ {
969
+ id: "EN-STR-STARTS",
970
+ match: /\b(?<field>\w+)\s+(?:startsWith|starts\s+with)\s+(?<prefix>\S+)/i,
971
+ spelTemplate: "#{field}.startsWith('{prefix}')",
972
+ slots: { prefix: { key: "prefix", type: "string" } },
973
+ priority: 84,
974
+ tags: ["string", "startsWith", "english"],
975
+ examples: [{ nl: "id starts with ORD", spel: "#id.startsWith('ORD')" }],
976
+ difficulty: "easy",
977
+ confidence: 0.95
978
+ },
979
+ {
980
+ id: "CN-STR-ENDS",
981
+ match: /^(?<field>[^\s,,、]+?)\s*以\s*(?<suffix>[^\s,,、]+)\s*(?:结尾|结束)/,
982
+ spelTemplate: "#{field}.endsWith('{suffix}')",
983
+ slots: { suffix: { key: "suffix", type: "string" } },
984
+ priority: 85,
985
+ tags: ["string", "endsWith"],
986
+ examples: [{ nl: "\u6587\u4EF6\u540D\u4EE5.pdf\u7ED3\u5C3E", spel: "#\u6587\u4EF6\u540D.endsWith('.pdf')" }],
987
+ difficulty: "easy",
988
+ confidence: 0.95
989
+ },
990
+ {
991
+ id: "EN-STR-ENDS",
992
+ match: /\b(?<field>\w+)\s+(?:endsWith|ends\s+with)\s+(?<suffix>\S+)/i,
993
+ spelTemplate: "#{field}.endsWith('{suffix}')",
994
+ slots: { suffix: { key: "suffix", type: "string" } },
995
+ priority: 84,
996
+ tags: ["string", "endsWith", "english"],
997
+ examples: [{ nl: "name endsWith .pdf", spel: "#name.endsWith('.pdf')" }],
998
+ difficulty: "easy",
999
+ confidence: 0.95
1000
+ },
1001
+ {
1002
+ id: "CN-STR-MATCHES",
1003
+ match: /^(?<field>[^\s,,、]+?)\s*(?:匹配|符合)\s*(?<pattern>[^\s,,、]+)/,
1004
+ spelTemplate: "#{field} matches '{pattern}'",
1005
+ slots: { pattern: { key: "pattern", type: "string" } },
1006
+ priority: 82,
1007
+ tags: ["string", "matches"],
1008
+ examples: [{ nl: "\u624B\u673A\u53F7\u5339\u914D\u6B63\u5219", spel: "#\u624B\u673A\u53F7 matches '\u6B63\u5219'" }],
1009
+ difficulty: "medium",
1010
+ confidence: 0.9
1011
+ },
1012
+ {
1013
+ id: "EN-STR-MATCHES",
1014
+ match: /\b(?<field>\w+)\s+matches\s+(?<pattern>\S+)/i,
1015
+ spelTemplate: "#{field} matches '{pattern}'",
1016
+ slots: { pattern: { key: "pattern", type: "string" } },
1017
+ priority: 81,
1018
+ tags: ["string", "matches", "english"],
1019
+ examples: [{ nl: "id matches REGEX", spel: "#id matches 'REGEX'" }],
1020
+ difficulty: "medium",
1021
+ confidence: 0.9
1022
+ },
1023
+ // ================================================================
1024
+ // Group 7: Range — pri 82
1025
+ // ================================================================
1026
+ {
1027
+ id: "CN-RANGE-BETWEEN",
1028
+ match: /^(?<field>[^\s,,、]+?)\s*(?:在|介于)\s*(?<min>\d+)\s*(?:和|到|~)\s*(?<max>\d+)\s*(?:之间|范围)?/,
1029
+ spelTemplate: "#{field} between {{{min}, {max}}}",
1030
+ slots: {
1031
+ min: { key: "min", type: "number", transform: "toNumber" },
1032
+ max: { key: "max", type: "number", transform: "toNumber" }
1033
+ },
1034
+ priority: 82,
1035
+ tags: ["range", "between"],
1036
+ examples: [{ nl: "\u5E74\u9F84\u572818\u523060\u4E4B\u95F4", spel: "#\u5E74\u9F84 between {18, 60}" }],
1037
+ difficulty: "easy",
1038
+ confidence: 0.95
1039
+ },
1040
+ {
1041
+ id: "EN-RANGE-BETWEEN",
1042
+ match: /\b(?<field>\w+)\s+between\s+(?<min>\d+)\s+and\s+(?<max>\d+)/i,
1043
+ spelTemplate: "#{field} between {{{min}, {max}}}",
1044
+ slots: {
1045
+ min: { key: "min", type: "number", transform: "toNumber" },
1046
+ max: { key: "max", type: "number", transform: "toNumber" }
1047
+ },
1048
+ priority: 82,
1049
+ tags: ["range", "between", "english"],
1050
+ examples: [{ nl: "amount between 100 and 500", spel: "#amount between {100, 500}" }],
1051
+ difficulty: "easy",
1052
+ confidence: 0.95
1053
+ },
1054
+ // ================================================================
1055
+ // Group 8: Elvis — pri 78
1056
+ // ================================================================
1057
+ {
1058
+ id: "CN-ELVIS",
1059
+ match: /^(?<field>[^\s,,、]+?)\s*(?:或者|或\s+|默认|否则)\s*(?<default>[^\s,,、]+)/,
1060
+ spelTemplate: "#{field} ?: '{default}'",
1061
+ slots: { default: { key: "default", type: "string" } },
1062
+ priority: 78,
1063
+ tags: ["elvis"],
1064
+ examples: [{ nl: "\u7528\u6237\u540D\u6216\u8005\u533F\u540D\u7528\u6237", spel: "#\u7528\u6237\u540D ?: '\u533F\u540D\u7528\u6237'" }],
1065
+ difficulty: "medium",
1066
+ confidence: 0.85
1067
+ },
1068
+ {
1069
+ id: "EN-ELVIS",
1070
+ match: /\b(?<field>\w+)\s+(?:or\s+default|default\s+to)\s+(?<default>\S+)/i,
1071
+ spelTemplate: "#{field} ?: '{default}'",
1072
+ slots: { default: { key: "default", type: "string" } },
1073
+ priority: 77,
1074
+ tags: ["elvis", "english"],
1075
+ examples: [{ nl: "name or default Guest", spel: "#name ?: 'Guest'" }],
1076
+ difficulty: "medium",
1077
+ confidence: 0.85
1078
+ },
1079
+ // ================================================================
1080
+ // Group 9: Date — pri 75
1081
+ // ================================================================
1082
+ {
1083
+ id: "CN-DATE-AFTER",
1084
+ match: /^(?<field>[^\s,,、]+?)\s*在\s*(?<date>[^\s,,、]+?)\s*之后/,
1085
+ spelTemplate: "#{field} > T(java.time.LocalDate).parse('{date}')",
1086
+ slots: { date: { key: "date", type: "string" } },
1087
+ priority: 75,
1088
+ tags: ["date", "after"],
1089
+ examples: [
1090
+ {
1091
+ nl: "\u521B\u5EFA\u65E5\u671F\u57282024-01-01\u4E4B\u540E",
1092
+ spel: "#\u521B\u5EFA\u65E5\u671F > T(java.time.LocalDate).parse('2024-01-01')"
1093
+ }
1094
+ ],
1095
+ difficulty: "easy",
1096
+ confidence: 0.9
1097
+ },
1098
+ {
1099
+ id: "CN-DATE-BEFORE",
1100
+ match: /^(?<field>[^\s,,、]+?)\s*在\s*(?<date>[^\s,,、]+?)\s*之前/,
1101
+ spelTemplate: "#{field} < T(java.time.LocalDate).parse('{date}')",
1102
+ slots: { date: { key: "date", type: "string" } },
1103
+ priority: 75,
1104
+ tags: ["date", "before"],
1105
+ examples: [
1106
+ {
1107
+ nl: "\u8FC7\u671F\u65E5\u671F\u57282025-12-31\u4E4B\u524D",
1108
+ spel: "#\u8FC7\u671F\u65E5\u671F < T(java.time.LocalDate).parse('2025-12-31')"
1109
+ }
1110
+ ],
1111
+ difficulty: "easy",
1112
+ confidence: 0.9
1113
+ },
1114
+ {
1115
+ id: "EN-DATE-AFTER",
1116
+ match: /\b(?<field>\w+)\s+after\s+(?<date>\S+)/i,
1117
+ spelTemplate: "#{field} > T(java.time.LocalDate).parse('{date}')",
1118
+ slots: { date: { key: "date", type: "string" } },
1119
+ priority: 75,
1120
+ tags: ["date", "after", "english"],
1121
+ examples: [
1122
+ { nl: "date after 2024-06-01", spel: "#date > T(java.time.LocalDate).parse('2024-06-01')" }
1123
+ ],
1124
+ difficulty: "easy",
1125
+ confidence: 0.9
1126
+ },
1127
+ {
1128
+ id: "EN-DATE-BEFORE",
1129
+ match: /\b(?<field>\w+)\s+before\s+(?<date>\S+)/i,
1130
+ spelTemplate: "#{field} < T(java.time.LocalDate).parse('{date}')",
1131
+ slots: { date: { key: "date", type: "string" } },
1132
+ priority: 75,
1133
+ tags: ["date", "before", "english"],
1134
+ examples: [
1135
+ { nl: "date before 2025-01-01", spel: "#date < T(java.time.LocalDate).parse('2025-01-01')" }
1136
+ ],
1137
+ difficulty: "easy",
1138
+ confidence: 0.9
1139
+ },
1140
+ // ================================================================
1141
+ // Group 10: Type check — pri 73
1142
+ // ================================================================
1143
+ {
1144
+ id: "CN-TYPE-INSTANCEOF",
1145
+ match: /^(?<field>[^\s,,、]+?)\s*(?:是否是|是否为|是|instanceof)\s*(?<type>[^\s,,、]+?)\s*(?:类型|类型|type|class)?$/i,
1146
+ spelTemplate: "#{field} instanceof T({type})",
1147
+ slots: { type: { key: "type", type: "variable" } },
1148
+ priority: 74,
1149
+ tags: ["type", "instanceof"],
1150
+ examples: [{ nl: "\u8D26\u53F7\u662F\u5426\u662FAdmin\u7C7B\u578B", spel: "#\u8D26\u53F7 instanceof T(Admin)" }],
1151
+ difficulty: "medium",
1152
+ confidence: 0.85
1153
+ },
1154
+ {
1155
+ id: "EN-TYPE-INSTANCEOF",
1156
+ match: /\b(?<field>\w+)\s+(?:is|instanceof)\s+(?<type>\w+)/i,
1157
+ spelTemplate: "#{field} instanceof T({type})",
1158
+ slots: { type: { key: "type", type: "variable" } },
1159
+ priority: 72,
1160
+ tags: ["type", "instanceof", "english"],
1161
+ examples: [{ nl: "account is Admin", spel: "#account instanceof T(Admin)" }],
1162
+ difficulty: "medium",
1163
+ confidence: 0.9
1164
+ },
1165
+ // ================================================================
1166
+ // Group 11: Boolean — pri 70-74
1167
+ // ================================================================
1168
+ {
1169
+ id: "CN-BOOL-TRUE",
1170
+ match: /^(?<field>[^\s,,、]+?)\s*(?:是|为|等于|==)\s*(?:true|真|是|yes)$/i,
1171
+ spelTemplate: "#{field} == true",
1172
+ slots: {},
1173
+ priority: 74,
1174
+ tags: ["boolean", "isTrue"],
1175
+ examples: [{ nl: "\u7528\u6237\u662FVIP", spel: "#\u7528\u6237 == true" }],
1176
+ difficulty: "easy",
1177
+ confidence: 0.92
1178
+ },
1179
+ {
1180
+ id: "CN-BOOL-FALSE",
1181
+ match: /^(?<field>[^\s,,、]+?)\s*(?:不是|非|为|是|等于|==)\s*(?:false|假|否|no)$/i,
1182
+ spelTemplate: "#{field} == false",
1183
+ slots: {},
1184
+ priority: 73,
1185
+ tags: ["boolean", "isFalse"],
1186
+ examples: [{ nl: "\u7528\u6237\u4E0D\u662FVIP", spel: "#\u7528\u6237 == false" }],
1187
+ difficulty: "easy",
1188
+ confidence: 0.92
1189
+ },
1190
+ {
1191
+ id: "EN-BOOL-TRUE",
1192
+ match: /^(?<field>\w+)\s+(?:is\s+)?true$/i,
1193
+ spelTemplate: "#{field} == true",
1194
+ slots: {},
1195
+ priority: 75,
1196
+ tags: ["boolean", "isTrue", "english"],
1197
+ examples: [{ nl: "isVIP is true", spel: "#isVIP == true" }],
1198
+ difficulty: "easy",
1199
+ confidence: 0.95
1200
+ },
1201
+ {
1202
+ id: "EN-BOOL-FALSE",
1203
+ match: /^(?<field>\w+)\s+(?:is\s+)?false$/i,
1204
+ spelTemplate: "#{field} == false",
1205
+ slots: {},
1206
+ priority: 75,
1207
+ tags: ["boolean", "isFalse", "english"],
1208
+ examples: [{ nl: "flag is false", spel: "#flag == false" }],
1209
+ difficulty: "easy",
1210
+ confidence: 0.95
1211
+ },
1212
+ {
1213
+ id: "CN-BOOL-FIELD",
1214
+ match: /^(?:是否|is\s+)(?<field>[^\s,,、]+?)(?:\?|吗|呢)?$/i,
1215
+ spelTemplate: "#{field}",
1216
+ slots: {},
1217
+ priority: 70,
1218
+ tags: ["boolean", "field"],
1219
+ examples: [{ nl: "\u662F\u5426\u5DF2\u652F\u4ED8", spel: "#\u5DF2\u652F\u4ED8" }],
1220
+ difficulty: "easy",
1221
+ confidence: 0.8
1222
+ },
1223
+ // ================================================================
1224
+ // Group 12: Logical combination — pri 63-65
1225
+ // ================================================================
1226
+ {
1227
+ id: "CN-LOGIC-AND",
1228
+ match: /(?<left>.+?)\s*(?:且|并且|而且|同时|&&)\s*(?<right>.+)/,
1229
+ spelTemplate: "({left}) and ({right})",
1230
+ slots: {
1231
+ left: { key: "left", type: "variable" },
1232
+ right: { key: "right", type: "variable" }
1233
+ },
1234
+ priority: 65,
1235
+ tags: ["logic", "and"],
1236
+ examples: [{ nl: "\u91D1\u989D\u5927\u4E8E1000\u4E14\u8BA2\u5355\u5DF2\u786E\u8BA4", spel: "(\u91D1\u989D\u5927\u4E8E1000) and (\u8BA2\u5355\u5DF2\u786E\u8BA4)" }],
1237
+ difficulty: "medium",
1238
+ confidence: 0.75
1239
+ },
1240
+ {
1241
+ id: "CN-LOGIC-OR",
1242
+ match: /(?<left>.+?)\s*(?:或|或者|\|\|)\s*(?<right>.+)/,
1243
+ spelTemplate: "({left}) or ({right})",
1244
+ slots: {
1245
+ left: { key: "left", type: "variable" },
1246
+ right: { key: "right", type: "variable" }
1247
+ },
1248
+ priority: 65,
1249
+ tags: ["logic", "or"],
1250
+ examples: [{ nl: "\u91D1\u989D\u5927\u4E8E1000\u6216\u5DF2\u53D1\u8D27", spel: "(\u91D1\u989D\u5927\u4E8E1000) or (\u5DF2\u53D1\u8D27)" }],
1251
+ difficulty: "medium",
1252
+ confidence: 0.75
1253
+ },
1254
+ {
1255
+ id: "EN-LOGIC-AND",
1256
+ match: /\b(?<left>.+?)\s+\band\b\s+(?<right>.+)/i,
1257
+ spelTemplate: "({left}) and ({right})",
1258
+ slots: {
1259
+ left: { key: "left", type: "variable" },
1260
+ right: { key: "right", type: "variable" }
1261
+ },
1262
+ priority: 64,
1263
+ tags: ["logic", "and", "english"],
1264
+ examples: [
1265
+ { nl: "amount > 100 and status == done", spel: "(amount > 100) and (status == done)" }
1266
+ ],
1267
+ difficulty: "medium",
1268
+ confidence: 0.75
1269
+ },
1270
+ {
1271
+ id: "EN-LOGIC-OR",
1272
+ match: /\b(?<left>.+?)\s+\bor\b\s+(?<right>.+)/i,
1273
+ spelTemplate: "({left}) or ({right})",
1274
+ slots: {
1275
+ left: { key: "left", type: "variable" },
1276
+ right: { key: "right", type: "variable" }
1277
+ },
1278
+ priority: 64,
1279
+ tags: ["logic", "or", "english"],
1280
+ examples: [{ nl: "VIP or amount > 1000", spel: "(VIP) or (amount > 1000)" }],
1281
+ difficulty: "medium",
1282
+ confidence: 0.75
1283
+ },
1284
+ // ================================================================
1285
+ // Group 13: Logical NOT — pri 61-62
1286
+ // ================================================================
1287
+ {
1288
+ id: "CN-LOGIC-NOT",
1289
+ match: /^不是\s+(?<expr>.+)/,
1290
+ spelTemplate: "!({expr})",
1291
+ slots: { expr: { key: "expr", type: "variable" } },
1292
+ priority: 62,
1293
+ tags: ["logic", "not"],
1294
+ examples: [{ nl: "\u4E0D\u662F\u6709\u6548", spel: "!(\u6709\u6548)" }],
1295
+ difficulty: "easy",
1296
+ confidence: 0.85
1297
+ },
1298
+ {
1299
+ id: "EN-LOGIC-NOT",
1300
+ match: /^not\s+(?<expr>.+)/i,
1301
+ spelTemplate: "!({expr})",
1302
+ slots: { expr: { key: "expr", type: "variable" } },
1303
+ priority: 61,
1304
+ tags: ["logic", "not", "english"],
1305
+ examples: [{ nl: "not cancelled", spel: "!(cancelled)" }],
1306
+ difficulty: "easy",
1307
+ confidence: 0.85
1308
+ },
1309
+ // ================================================================
1310
+ // Group 14: Projection & selection — pri 55-60
1311
+ // ================================================================
1312
+ {
1313
+ id: "CN-SELECT-FIRST",
1314
+ match: /^(?<root>[^\s,,、]+?)\s*中\s*第一[个位]\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==)\s*(?<value>[^\s,,、]+)/,
1315
+ spelTemplate: "#{root}.items.^[#{this}.{field} > {value}]",
1316
+ slots: {
1317
+ root: { key: "root", type: "variable" },
1318
+ field: { key: "field", type: "variable" },
1319
+ value: { key: "value", type: "literal" }
1320
+ },
1321
+ priority: 60,
1322
+ tags: ["selection", "first"],
1323
+ examples: [{ nl: "\u8BA2\u5355\u4E2D\u7B2C\u4E00\u4E2A\u91D1\u989D\u5927\u4E8E1000\u7684", spel: "#\u8BA2\u5355.items.^[#this.\u91D1\u989D > 1000]" }],
1324
+ difficulty: "medium",
1325
+ confidence: 0.8
1326
+ },
1327
+ {
1328
+ id: "CN-SELECT-ALL",
1329
+ match: /^(?<root>[^\s,,、]+?)\s*中\s*所有\s*(?<field>[^\s,,、]+?)\s*(?:大于|>|超过|<|小于|等于|==|包含|满足)\s*(?<value>[^\s,,、]+)/,
1330
+ spelTemplate: "#{root}.items.?[#{this}.{field} > {value}]",
1331
+ slots: {
1332
+ root: { key: "root", type: "variable" },
1333
+ field: { key: "field", type: "variable" },
1334
+ value: { key: "value", type: "literal" }
1335
+ },
1336
+ priority: 60,
1337
+ tags: ["selection", "all"],
1338
+ examples: [{ nl: "\u8BA2\u5355\u4E2D\u6240\u6709\u91D1\u989D\u5927\u4E8E1000\u7684", spel: "#\u8BA2\u5355.items.?[#this.\u91D1\u989D > 1000]" }],
1339
+ difficulty: "medium",
1340
+ confidence: 0.8
1341
+ },
1342
+ {
1343
+ id: "CN-PROJ",
1344
+ match: /^(?<root>[^\s,,、]+?)\s*中\s*每[个一]\s*(?:的)?(?<field>[^\s,,、]+?)\s*(?:值|金额|名称|价格|name|amount|price)?/,
1345
+ spelTemplate: "#{root}.items.![#{this}.{field}]",
1346
+ slots: {
1347
+ root: { key: "root", type: "variable" },
1348
+ field: { key: "field", type: "variable" }
1349
+ },
1350
+ priority: 55,
1351
+ tags: ["projection"],
1352
+ examples: [{ nl: "\u8BA2\u5355\u4E2D\u6BCF\u4E2A\u5546\u54C1\u7684\u4EF7\u683C", spel: "#\u8BA2\u5355.items.![#this.\u5546\u54C1]" }],
1353
+ difficulty: "medium",
1354
+ confidence: 0.75
1355
+ },
1356
+ {
1357
+ id: "EN-SELECT-ALL",
1358
+ match: /all\s+(?<root>\w+)\s+with\s+(?<field>\w+)\s*>\s*(?<value>\d+)/i,
1359
+ spelTemplate: "#{root}.items.?[#{this}.{field} > {value}]",
1360
+ slots: {
1361
+ root: { key: "root", type: "variable" },
1362
+ field: { key: "field", type: "variable" },
1363
+ value: { key: "value", type: "number", transform: "toNumber" }
1364
+ },
1365
+ priority: 50,
1366
+ tags: ["selection", "all", "english"],
1367
+ examples: [{ nl: "all items with price > 100", spel: "#items.items.?[#this.price > 100]" }],
1368
+ difficulty: "medium",
1369
+ confidence: 0.8
1370
+ }
1371
+ ];
1372
+ BUILTIN_PATTERNS.sort((a, b) => b.priority - a.priority);
1373
+
1374
+ // src/template/nl-intent.ts
1375
+ var NLIntent = /* @__PURE__ */ ((NLIntent2) => {
1376
+ NLIntent2["COMPARISON"] = "COMPARISON";
1377
+ NLIntent2["PERMISSION_CHECK"] = "PERMISSION_CHECK";
1378
+ NLIntent2["NULL_CHECK"] = "NULL_CHECK";
1379
+ NLIntent2["LOGICAL"] = "LOGICAL";
1380
+ NLIntent2["STRING_MATCH"] = "STRING_MATCH";
1381
+ NLIntent2["COLLECTION"] = "COLLECTION";
1382
+ NLIntent2["RANGE"] = "RANGE";
1383
+ NLIntent2["SELECTION"] = "SELECTION";
1384
+ NLIntent2["PROJECTION"] = "PROJECTION";
1385
+ NLIntent2["TYPE_CHECK"] = "TYPE_CHECK";
1386
+ NLIntent2["BOOLEAN"] = "BOOLEAN";
1387
+ NLIntent2["DATE"] = "DATE";
1388
+ NLIntent2["ELVIS"] = "ELVIS";
1389
+ NLIntent2["ASSIGNMENT"] = "ASSIGNMENT";
1390
+ NLIntent2["ARITHMETIC"] = "ARITHMETIC";
1391
+ return NLIntent2;
1392
+ })(NLIntent || {});
1393
+
1394
+ // src/template/intent-classifier.ts
1395
+ var INTENT_KEYWORDS = {
1396
+ ["COMPARISON" /* COMPARISON */]: {
1397
+ zh: [
1398
+ "\u5927\u4E8E",
1399
+ "\u5C0F\u4E8E",
1400
+ "\u7B49\u4E8E",
1401
+ "\u8D85\u8FC7",
1402
+ "\u4F4E\u4E8E",
1403
+ "\u4E0D\u5C0F\u4E8E",
1404
+ "\u4E0D\u5927\u4E8E",
1405
+ "\u5927\u4E8E\u7B49\u4E8E",
1406
+ "\u5C0F\u4E8E\u7B49\u4E8E",
1407
+ "\u4E0D\u7B49\u4E8E"
1408
+ ],
1409
+ en: ["greater", "less", "equal", "above", "below", "exceed", ">", "<", "==", "!=", ">=", "<="]
1410
+ },
1411
+ ["NULL_CHECK" /* NULL_CHECK */]: {
1412
+ zh: ["\u4E3A\u7A7A", "\u4E0D\u4E3A\u7A7A", "\u662F\u7A7A", "\u5B58\u5728", "\u4E0D\u5B58\u5728", "null", "\u6CA1\u6709\u503C"],
1413
+ en: ["null", "empty", "is null", "is not null", "is empty", "is not empty"]
1414
+ },
1415
+ ["PERMISSION_CHECK" /* PERMISSION_CHECK */]: {
1416
+ zh: ["\u89D2\u8272", "\u6743\u9650", "\u7BA1\u7406\u5458", "\u53EF\u4EE5", "\u5141\u8BB8", "\u7981\u6B62", "\u62D2\u7EDD"],
1417
+ en: ["role", "permission", "can", "allow", "deny", "has role", "has permission"]
1418
+ },
1419
+ ["LOGICAL" /* LOGICAL */]: {
1420
+ zh: ["\u4E14", "\u5E76\u4E14", "\u540C\u65F6", "\u6216", "\u6216\u8005", "\u975E", "\u4E0D\u662F", "\u4E5F\u4E0D"],
1421
+ en: ["and", "or", "not", "&&", "||", "!"]
1422
+ },
1423
+ ["STRING_MATCH" /* STRING_MATCH */]: {
1424
+ zh: ["\u5305\u542B", "\u542B\u6709", "\u5305\u62EC", "\u4EE5...\u5F00\u5934", "\u4EE5...\u7ED3\u5C3E", "\u5339\u914D", "\u7B26\u5408", "\u6B63\u5219", "\u5F00\u5934", "\u7ED3\u5C3E"],
1425
+ en: ["contains", "includes", "starts with", "ends with", "matches", "regex"]
1426
+ },
1427
+ ["COLLECTION" /* COLLECTION */]: {
1428
+ zh: ["\u5217\u8868", "\u6570\u7EC4", "\u96C6\u5408", "\u5927\u5C0F", "\u957F\u5EA6", "isEmpty", "\u4E3A\u7A7A", "\u4E0D\u4E3A\u7A7A", "\u9879", "items"],
1429
+ en: ["list", "array", "collection", "size", "length", "count", "isempty", "empty", "items"]
1430
+ },
1431
+ ["RANGE" /* RANGE */]: {
1432
+ zh: ["\u4E4B\u95F4", "\u8303\u56F4", "\u4ECB\u4E8E", "between"],
1433
+ en: ["between", "range", "from...to"]
1434
+ },
1435
+ ["SELECTION" /* SELECTION */]: {
1436
+ zh: ["\u7B5B\u9009", "\u9009\u62E9", "\u6240\u6709\u6EE1\u8DB3", "\u7B2C\u4E00\u4E2A\u6EE1\u8DB3", "\u7B26\u5408\u6761\u4EF6\u7684"],
1437
+ en: ["select", "filter", "first", "matching"]
1438
+ },
1439
+ ["PROJECTION" /* PROJECTION */]: {
1440
+ zh: ["\u6BCF\u4E2A", "\u6240\u6709", "\u5168\u90E8", "\u6295\u5F71"],
1441
+ en: ["each", "every", "project"]
1442
+ },
1443
+ ["TYPE_CHECK" /* TYPE_CHECK */]: {
1444
+ zh: ["\u662F\u5426\u662F", "\u7C7B\u578B", "instanceof"],
1445
+ en: ["instanceof", "type of", "is a"]
1446
+ },
1447
+ ["BOOLEAN" /* BOOLEAN */]: {
1448
+ zh: ["\u662F\u5426", "\u771F\u5047", "true", "false", "\u662F", "\u5426"],
1449
+ en: ["true", "false", "yes", "no", "is", "is not"]
1450
+ },
1451
+ ["DATE" /* DATE */]: {
1452
+ zh: ["\u65E5\u671F", "\u65F6\u95F4", "\u4E4B\u540E", "\u4E4B\u524D", "\u65E9\u4E8E", "\u665A\u4E8E"],
1453
+ en: ["date", "time", "after", "before", "earlier", "later"]
1454
+ },
1455
+ ["ELVIS" /* ELVIS */]: {
1456
+ zh: ["\u6216\u8005", "\u9ED8\u8BA4\u503C", "\u515C\u5E95", "\u9ED8\u8BA4", "\u5426\u5219"],
1457
+ en: ["default", "fallback", "or", "otherwise"]
1458
+ },
1459
+ ["ASSIGNMENT" /* ASSIGNMENT */]: {
1460
+ zh: ["\u8BBE\u7F6E", "\u8D4B\u503C", "\u8D4B\u4E88", "\u8BBE\u5B9A"],
1461
+ en: ["set", "assign", "put"]
1462
+ },
1463
+ ["ARITHMETIC" /* ARITHMETIC */]: {
1464
+ zh: ["\u52A0", "\u51CF", "\u4E58", "\u9664", "\u53D6\u6A21", "\u6C42\u548C", "\u5E73\u5747\u503C", "\u603B\u548C"],
1465
+ en: ["plus", "minus", "multiply", "divide", "mod", "sum", "average"]
1466
+ }
1467
+ };
1468
+ var IntentClassifier = class {
1469
+ /**
1470
+ * Main method for classifying natural language input
1471
+ */
1472
+ classify(input) {
1473
+ const normalized = this.normalize(input);
1474
+ const intentScores = /* @__PURE__ */ new Map();
1475
+ for (const intent of Object.keys(INTENT_KEYWORDS)) {
1476
+ const keywords = INTENT_KEYWORDS[intent];
1477
+ let score = 0;
1478
+ for (const kw of keywords.zh) {
1479
+ if (normalized.includes(kw)) score += 3;
1480
+ }
1481
+ for (const kw of keywords.en) {
1482
+ if (normalized.toLowerCase().includes(kw.toLowerCase())) score += 2;
1483
+ }
1484
+ if (score > 0) intentScores.set(intent, score);
1485
+ }
1486
+ if (intentScores.has("LOGICAL" /* LOGICAL */) && intentScores.has("COMPARISON" /* COMPARISON */)) {
1487
+ intentScores.set("LOGICAL" /* LOGICAL */, (intentScores.get("LOGICAL" /* LOGICAL */) ?? 0) + 2);
1488
+ }
1489
+ if (/between\s+\S+\s+(and|,)\s+\S+/i.test(normalized)) {
1490
+ intentScores.set("RANGE" /* RANGE */, (intentScores.get("RANGE" /* RANGE */) ?? 0) + 3);
1491
+ }
1492
+ if (/instanceof/i.test(normalized) || /是否是/.test(normalized) && /类型/.test(normalized)) {
1493
+ intentScores.set("TYPE_CHECK" /* TYPE_CHECK */, (intentScores.get("TYPE_CHECK" /* TYPE_CHECK */) ?? 0) + 3);
1494
+ }
1495
+ if (/每个/.test(normalized) || /each/.test(normalized)) {
1496
+ intentScores.set("PROJECTION" /* PROJECTION */, (intentScores.get("PROJECTION" /* PROJECTION */) ?? 0) + 2);
1497
+ }
1498
+ if (/isempty|\.isEmpty|列表|数组|集合/.test(normalized)) {
1499
+ intentScores.set("COLLECTION" /* COLLECTION */, (intentScores.get("COLLECTION" /* COLLECTION */) ?? 0) + 1);
1500
+ }
1501
+ const entities = this.extractEntities(normalized);
1502
+ const operators = this.extractOperators(normalized);
1503
+ const logicalConnectors = this.extractLogicalConnectors(normalized);
1504
+ const complexity = this.calculateComplexity(
1505
+ normalized,
1506
+ intentScores,
1507
+ entities,
1508
+ operators,
1509
+ logicalConnectors
1510
+ );
1511
+ const sortedIntents = Array.from(intentScores.entries()).sort((a, b) => b[1] - a[1]).map(([intent, score]) => ({
1512
+ intent,
1513
+ confidence: Math.min(score / 15, 1)
1514
+ }));
1515
+ return {
1516
+ primaryIntent: sortedIntents[0]?.intent ?? "COMPARISON" /* COMPARISON */,
1517
+ intents: sortedIntents.length > 0 ? sortedIntents : [{ intent: "COMPARISON" /* COMPARISON */, confidence: 0.3 }],
1518
+ entities,
1519
+ operators,
1520
+ logicalConnectors,
1521
+ complexity
1522
+ };
1523
+ }
1524
+ /**
1525
+ * Entity extraction
1526
+ */
1527
+ extractEntities(normalized) {
1528
+ const entities = [];
1529
+ const numRegex = /\b(\d+(?:\.\d+)?)\b/g;
1530
+ let match;
1531
+ while ((match = numRegex.exec(normalized)) !== null) {
1532
+ entities.push({
1533
+ text: match[1],
1534
+ type: "value",
1535
+ position: { start: match.index, end: match.index + match[1].length }
1536
+ });
1537
+ }
1538
+ const strRegex = /['""]([^'""]*)['""]/g;
1539
+ while ((match = strRegex.exec(normalized)) !== null) {
1540
+ entities.push({
1541
+ text: match[1],
1542
+ type: "value",
1543
+ position: { start: match.index, end: match.index + match[0].length }
1544
+ });
1545
+ }
1546
+ for (const op of [">=", "<=", "!=", "==", ">", "<", "="]) {
1547
+ let idx = normalized.indexOf(op);
1548
+ if (idx >= 0) {
1549
+ entities.push({
1550
+ text: op,
1551
+ type: "operator",
1552
+ position: { start: idx, end: idx + op.length }
1553
+ });
1554
+ }
1555
+ }
1556
+ return entities;
1557
+ }
1558
+ /**
1559
+ * Operator recognition
1560
+ */
1561
+ extractOperators(normalized) {
1562
+ const operators = [];
1563
+ const OP_MAP = [
1564
+ { pattern: /大于等于|>=|≥/, op: ">=" },
1565
+ { pattern: /小于等于|<=|≤/, op: "<=" },
1566
+ { pattern: /不等于|!=|≠/, op: "!=" },
1567
+ { pattern: /等于|==/, op: "==" },
1568
+ { pattern: /大于|>/, op: ">" },
1569
+ { pattern: /小于|</, op: "<" },
1570
+ { pattern: /匹配|match/, op: "matches" },
1571
+ { pattern: /之间|between/, op: "between" },
1572
+ { pattern: /包含|contains/, op: "contains" }
1573
+ ];
1574
+ for (const { pattern, op } of OP_MAP) {
1575
+ if (pattern.test(normalized)) {
1576
+ operators.push(op);
1577
+ }
1578
+ }
1579
+ return operators;
1580
+ }
1581
+ /**
1582
+ * Logical connector recognition
1583
+ */
1584
+ extractLogicalConnectors(normalized) {
1585
+ const connectors = [];
1586
+ const isBetween = /between\s+\d+(\s*,\s*|\s+and\s+)\d+/i.test(normalized);
1587
+ if (/且|并且|and|&&/i.test(normalized) && !isBetween) connectors.push("and");
1588
+ if (/或|或者|or|\|\|/i.test(normalized)) connectors.push("or");
1589
+ if (/非|不是|不|not|!/i.test(normalized)) connectors.push("not");
1590
+ return connectors;
1591
+ }
1592
+ /**
1593
+ * Complexity calculation
1594
+ */
1595
+ calculateComplexity(input, intentScores, entities, operators, logicalConnectors) {
1596
+ let complexity = 0;
1597
+ complexity += Math.min(input.length / 5, 15);
1598
+ complexity += (intentScores.size - 1) * 10;
1599
+ complexity += logicalConnectors.length * 15;
1600
+ complexity += entities.length * 5;
1601
+ complexity += operators.length * 8;
1602
+ return Math.max(0, Math.min(complexity, 100));
1603
+ }
1604
+ /**
1605
+ * Input normalization
1606
+ */
1607
+ normalize(input) {
1608
+ return input.trim().toLowerCase().replace(/[\uFF01-\uFF5E]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 65248)).replace(/\s+/g, " ");
1609
+ }
1610
+ };
1611
+
1612
+ // src/template/template-engine.ts
1613
+ var TEMPLATE_LIBRARY = {
1614
+ ["COMPARISON" /* COMPARISON */]: [
1615
+ {
1616
+ name: "COMPARISON-SIMPLE",
1617
+ template: "#{root}.{field} {operator} {value}",
1618
+ conditions: { hasComparison: true, entityCount: { min: 1 } },
1619
+ confidence: 0.9
1620
+ },
1621
+ {
1622
+ name: "COMPARISON-DUAL",
1623
+ template: "(#{root}.{field1} {operator1} {value1}) {logic} (#{root}.{field2} {operator2} {value2})",
1624
+ conditions: { hasComparison: true, hasLogical: true, entityCount: { min: 2 } },
1625
+ confidence: 0.8
1626
+ }
1627
+ ],
1628
+ ["NULL_CHECK" /* NULL_CHECK */]: [
1629
+ {
1630
+ name: "NULL-IS_NULL",
1631
+ template: "#{root}.{field} == null",
1632
+ conditions: { hasNull: true },
1633
+ confidence: 0.95
1634
+ },
1635
+ {
1636
+ name: "NULL-IS_NOT_NULL",
1637
+ template: "#{root}.{field} != null",
1638
+ conditions: { hasNull: true },
1639
+ confidence: 0.95
1640
+ }
1641
+ ],
1642
+ ["PERMISSION_CHECK" /* PERMISSION_CHECK */]: [
1643
+ {
1644
+ name: "PERM-HAS_ROLE",
1645
+ template: "hasRole('{role}')",
1646
+ conditions: {},
1647
+ confidence: 0.9
1648
+ },
1649
+ {
1650
+ name: "PERM-HAS_PERMISSION",
1651
+ template: "hasPermission('{permission}')",
1652
+ conditions: {},
1653
+ confidence: 0.9
1654
+ }
1655
+ ],
1656
+ ["LOGICAL" /* LOGICAL */]: [
1657
+ {
1658
+ name: "LOGICAL-AND",
1659
+ template: "({left}) and ({right})",
1660
+ conditions: { hasLogical: true },
1661
+ confidence: 0.75
1662
+ },
1663
+ {
1664
+ name: "LOGICAL-OR",
1665
+ template: "({left}) or ({right})",
1666
+ conditions: { hasLogical: true },
1667
+ confidence: 0.75
1668
+ },
1669
+ {
1670
+ name: "LOGICAL-NOT",
1671
+ template: "!({expr})",
1672
+ conditions: { hasLogical: true },
1673
+ confidence: 0.85
1674
+ }
1675
+ ],
1676
+ ["STRING_MATCH" /* STRING_MATCH */]: [
1677
+ {
1678
+ name: "STR-CONTAINS",
1679
+ template: "#{root}.{field}.contains('{substring}')",
1680
+ conditions: { hasString: true },
1681
+ confidence: 0.9
1682
+ },
1683
+ {
1684
+ name: "STR-STARTS_WITH",
1685
+ template: "#{root}.{field}.startsWith('{prefix}')",
1686
+ conditions: { hasString: true },
1687
+ confidence: 0.9
1688
+ },
1689
+ {
1690
+ name: "STR-ENDS_WITH",
1691
+ template: "#{root}.{field}.endsWith('{suffix}')",
1692
+ conditions: { hasString: true },
1693
+ confidence: 0.9
1694
+ },
1695
+ {
1696
+ name: "STR-MATCHES",
1697
+ template: "#{root}.{field} matches '{pattern}'",
1698
+ conditions: { hasString: true },
1699
+ confidence: 0.8
1700
+ }
1701
+ ],
1702
+ ["COLLECTION" /* COLLECTION */]: [
1703
+ {
1704
+ name: "COLL-IS_EMPTY",
1705
+ template: "#{root}.{list}.isEmpty()",
1706
+ conditions: { hasCollection: true },
1707
+ confidence: 0.95
1708
+ },
1709
+ {
1710
+ name: "COLL-IS_NOT_EMPTY",
1711
+ template: "!#{root}.{list}.isEmpty()",
1712
+ conditions: { hasCollection: true },
1713
+ confidence: 0.9
1714
+ },
1715
+ {
1716
+ name: "COLL-SIZE",
1717
+ template: "#{root}.{list}.size() {operator} {value}",
1718
+ conditions: { hasCollection: true, hasComparison: true },
1719
+ confidence: 0.85
1720
+ },
1721
+ {
1722
+ name: "COLL-CONTAINS",
1723
+ template: "#{root}.{list}.contains('{element}')",
1724
+ conditions: { hasCollection: true },
1725
+ confidence: 0.9
1726
+ }
1727
+ ],
1728
+ ["RANGE" /* RANGE */]: [
1729
+ {
1730
+ name: "RANGE-BETWEEN",
1731
+ template: "#{root}.{field} between {{{min}, {max}}}",
1732
+ conditions: { entityCount: { min: 1 } },
1733
+ confidence: 0.9
1734
+ }
1735
+ ],
1736
+ ["SELECTION" /* SELECTION */]: [
1737
+ {
1738
+ name: "SELECT-ALL",
1739
+ template: "#{root}.{list}.?[#{this}.{conditionField} {conditionOp} {conditionValue}]",
1740
+ conditions: {},
1741
+ confidence: 0.75
1742
+ },
1743
+ {
1744
+ name: "SELECT-FIRST",
1745
+ template: "#{root}.{list}.^[#{this}.{conditionField} {conditionOp} {conditionValue}]",
1746
+ conditions: {},
1747
+ confidence: 0.75
1748
+ }
1749
+ ],
1750
+ ["PROJECTION" /* PROJECTION */]: [
1751
+ {
1752
+ name: "PROJ-FIELD",
1753
+ template: "#{root}.{list}.![#{this}.{field}]",
1754
+ conditions: {},
1755
+ confidence: 0.75
1756
+ }
1757
+ ],
1758
+ ["TYPE_CHECK" /* TYPE_CHECK */]: [
1759
+ {
1760
+ name: "TYPE-INSTANCEOF",
1761
+ template: "#{root}.{field} instanceof T({typeName})",
1762
+ conditions: {},
1763
+ confidence: 0.85
1764
+ }
1765
+ ],
1766
+ ["BOOLEAN" /* BOOLEAN */]: [
1767
+ {
1768
+ name: "BOOL-IS_TRUE",
1769
+ template: "#{root}.{field} == true",
1770
+ conditions: {},
1771
+ confidence: 0.9
1772
+ },
1773
+ {
1774
+ name: "BOOL-IS_FALSE",
1775
+ template: "#{root}.{field} == false",
1776
+ conditions: {},
1777
+ confidence: 0.9
1778
+ },
1779
+ {
1780
+ name: "BOOL-FIELD",
1781
+ template: "#{root}.{field}",
1782
+ conditions: {},
1783
+ confidence: 0.85
1784
+ }
1785
+ ],
1786
+ ["DATE" /* DATE */]: [
1787
+ {
1788
+ name: "DATE-AFTER",
1789
+ template: "#{root}.{field}.after(T(java.util.Date).valueOf('{date}'))",
1790
+ conditions: {},
1791
+ confidence: 0.85
1792
+ },
1793
+ {
1794
+ name: "DATE-BEFORE",
1795
+ template: "#{root}.{field}.before(T(java.util.Date).valueOf('{date}'))",
1796
+ conditions: {},
1797
+ confidence: 0.85
1798
+ }
1799
+ ],
1800
+ ["ELVIS" /* ELVIS */]: [
1801
+ {
1802
+ name: "ELVIS-DEFAULT",
1803
+ template: "#{root}.{field} ?: '{defaultValue}'",
1804
+ conditions: {},
1805
+ confidence: 0.8
1806
+ }
1807
+ ],
1808
+ ["ASSIGNMENT" /* ASSIGNMENT */]: [
1809
+ {
1810
+ name: "ASSIGN-SIMPLE",
1811
+ template: "#{root}.{field} = {value}",
1812
+ conditions: {},
1813
+ confidence: 0.8
1814
+ }
1815
+ ],
1816
+ ["ARITHMETIC" /* ARITHMETIC */]: [
1817
+ {
1818
+ name: "ARITH-OP",
1819
+ template: "#{root}.{field1} {operator} #{root}.{field2}",
1820
+ conditions: {},
1821
+ confidence: 0.75
1822
+ }
1823
+ ]
1824
+ };
1825
+ var TemplateEngine = class {
1826
+ contextSchema;
1827
+ constructor(contextSchema) {
1828
+ this.contextSchema = contextSchema;
1829
+ }
1830
+ setContext(schema) {
1831
+ this.contextSchema = schema;
1832
+ }
1833
+ /**
1834
+ * Generate a SpEL expression based on intent classification results
1835
+ */
1836
+ generate(input, intentResult) {
1837
+ const templates = TEMPLATE_LIBRARY[intentResult.primaryIntent];
1838
+ if (!templates || templates.length === 0) return null;
1839
+ const bestTemplate = this.selectBestTemplate(templates, intentResult, input);
1840
+ if (!bestTemplate) return null;
1841
+ const { expression, unfilledSlots } = this.fillTemplate(
1842
+ bestTemplate.template,
1843
+ input,
1844
+ intentResult
1845
+ );
1846
+ return {
1847
+ expression,
1848
+ intent: intentResult.primaryIntent,
1849
+ templateName: bestTemplate.name,
1850
+ confidence: bestTemplate.confidence * (intentResult.intents[0]?.confidence ?? 0.5),
1851
+ unfilledSlots
1852
+ };
1853
+ }
1854
+ selectBestTemplate(templates, intentResult, input) {
1855
+ let bestScore = -1;
1856
+ let bestTemplate = null;
1857
+ const hasEmptyKeyword = /为空|empty|null/i.test(input);
1858
+ const hasNotEmptyKeyword = /不为空|not empty|not null/i.test(input);
1859
+ for (const template of templates) {
1860
+ const conditions = template.conditions;
1861
+ let score = 0;
1862
+ if (conditions.hasComparison && intentResult.operators.length > 0) score += 1;
1863
+ if (conditions.hasLogical && intentResult.logicalConnectors.length > 0) score += 1;
1864
+ if (conditions.hasCollection) score += 0.5;
1865
+ if (conditions.hasNull) score += 0.5;
1866
+ if (conditions.hasString) score += 1;
1867
+ if (template.name.includes("IS_EMPTY") && hasEmptyKeyword) score += 2;
1868
+ if (template.name.includes("IS_NOT_EMPTY") && hasNotEmptyKeyword) score += 2;
1869
+ if (conditions.entityCount) {
1870
+ if (conditions.entityCount.min && intentResult.entities.length < conditions.entityCount.min) {
1871
+ continue;
1872
+ }
1873
+ }
1874
+ if (score > bestScore) {
1875
+ bestScore = score;
1876
+ bestTemplate = template;
1877
+ }
1878
+ }
1879
+ return bestTemplate;
1880
+ }
1881
+ fillTemplate(template, input, intentResult) {
1882
+ let expression = template;
1883
+ const unfilledSlots = [];
1884
+ let rootName = "order";
1885
+ let fieldName = "field";
1886
+ if (this.contextSchema?.root) {
1887
+ rootName = this.contextSchema.root.name;
1888
+ const fields = Object.keys(this.contextSchema.root.fields ?? {});
1889
+ for (const f of fields) {
1890
+ if (input.includes(f)) {
1891
+ fieldName = f;
1892
+ break;
1893
+ }
1894
+ }
1895
+ } else {
1896
+ const rootMap = {
1897
+ \u8BA2\u5355: "order",
1898
+ order: "order",
1899
+ \u7528\u6237: "user",
1900
+ user: "user",
1901
+ \u6587\u4EF6: "file",
1902
+ file: "file",
1903
+ \u8D26\u53F7: "account",
1904
+ account: "account",
1905
+ \u5546\u54C1: "item",
1906
+ product: "item"
1907
+ };
1908
+ for (const [key, val] of Object.entries(rootMap)) {
1909
+ if (input.includes(key)) {
1910
+ rootName = val;
1911
+ break;
1912
+ }
1913
+ }
1914
+ }
1915
+ expression = expression.replace(/\{root\}/g, rootName);
1916
+ expression = expression.replace(/\{field\}/g, fieldName);
1917
+ const fieldEntities = intentResult.entities.filter((e) => e.type === "field");
1918
+ const field1Name = fieldEntities[0]?.text ?? fieldName;
1919
+ const field2Name = fieldEntities[1]?.text ?? fieldName;
1920
+ expression = expression.replace(/\{field1\}/g, field1Name);
1921
+ expression = expression.replace(/\{field2\}/g, field2Name);
1922
+ expression = expression.replace(/\{conditionField\}/g, "condition");
1923
+ expression = expression.replace(/\{list\}/g, "items");
1924
+ const numberEntities = intentResult.entities.filter(
1925
+ (e) => e.type === "value" && !isNaN(Number(e.text))
1926
+ );
1927
+ const stringEntities = intentResult.entities.filter(
1928
+ (e) => e.type === "value" && isNaN(Number(e.text))
1929
+ );
1930
+ if (numberEntities.length > 0) {
1931
+ expression = expression.replace(/\{value\}/, numberEntities[0].text);
1932
+ expression = expression.replace(/\{value1\}/, numberEntities[0].text);
1933
+ expression = expression.replace(/\{value2\}/, (numberEntities[1] ?? numberEntities[0]).text);
1934
+ expression = expression.replace(/\{min\}/, numberEntities[0].text);
1935
+ expression = expression.replace(/\{max\}/, (numberEntities[1] ?? numberEntities[0]).text);
1936
+ expression = expression.replace(/\{conditionValue\}/, numberEntities[0].text);
1937
+ }
1938
+ if (stringEntities.length > 0) {
1939
+ expression = expression.replace(/\{substring\}/, stringEntities[0].text);
1940
+ expression = expression.replace(/\{element\}/, stringEntities[0].text);
1941
+ expression = expression.replace(/\{role\}/, stringEntities[0].text);
1942
+ expression = expression.replace(/\{permission\}/, stringEntities[0].text);
1943
+ expression = expression.replace(/\{prefix\}/, stringEntities[0].text);
1944
+ expression = expression.replace(/\{suffix\}/, stringEntities[0].text);
1945
+ expression = expression.replace(/\{pattern\}/, stringEntities[0].text);
1946
+ expression = expression.replace(/\{defaultValue\}/, stringEntities[0].text);
1947
+ expression = expression.replace(/\{date\}/, stringEntities[0].text);
1948
+ expression = expression.replace(/\{typeName\}/, stringEntities[0].text);
1949
+ }
1950
+ if (intentResult.operators.length > 0) {
1951
+ expression = expression.replace(/\{operator\}/g, intentResult.operators[0]);
1952
+ expression = expression.replace(/\{operator1\}/g, intentResult.operators[0]);
1953
+ expression = expression.replace(
1954
+ /\{operator2\}/g,
1955
+ intentResult.operators[1] ?? intentResult.operators[0]
1956
+ );
1957
+ expression = expression.replace(/\{conditionOp\}/g, intentResult.operators[0]);
1958
+ }
1959
+ if (intentResult.logicalConnectors.length > 0) {
1960
+ expression = expression.replace(/\{logic\}/g, intentResult.logicalConnectors[0]);
1961
+ } else {
1962
+ expression = expression.replace(/\{logic\}/g, "and");
1963
+ }
1964
+ const unfilledMatches = expression.matchAll(/\{(\w+)\}/g);
1965
+ for (const m of unfilledMatches) {
1966
+ const slot = m[1];
1967
+ const defaults = {
1968
+ operator: "==",
1969
+ operator1: "==",
1970
+ operator2: "==",
1971
+ logic: "and",
1972
+ conditionOp: "=="
1973
+ };
1974
+ if (defaults[slot]) {
1975
+ expression = expression.replace(new RegExp(`\\{${slot}\\}`, "g"), defaults[slot]);
1976
+ } else {
1977
+ unfilledSlots.push(slot);
1978
+ expression = expression.replace(new RegExp(`\\{${slot}\\}`, "g"), `\${${slot}}`);
1979
+ }
1980
+ }
1981
+ return { expression, unfilledSlots };
1982
+ }
1983
+ };
1984
+
1985
+ // src/template/prompts/prompt-builder.ts
1986
+ var SYSTEM_PROMPT_ZH = `You are a Spring Expression Language (SpEL) expert. Your task is to convert natural language descriptions into precise SpEL expressions.
1987
+
1988
+ ## Rules
1989
+ 1. SpEL uses #variable to reference variables, and #root.field to reference root object fields
1990
+ 2. String literals use single quotes: 'value'
1991
+ 3. Null comparison: == null or != null
1992
+ 4. Logical operators: and, or, not (NOT &&, ||, !)
1993
+ 5. Comparison operators: ==, !=, >, <, >=, <=
1994
+ 6. Ternary operator uses Elvis syntax: value ?: default
1995
+ 7. Method calls: list.contains('item'), list.size(), list.isEmpty()
1996
+ 8. Collection projection: list.?[condition], list.^[condition], list.![projection]
1997
+ 9. Range check: value between {min, max}
1998
+ 10. Type check: variable instanceof T(ClassName)
1999
+ 11. Permission check: hasRole('role'), hasPermission('permission')
2000
+ 12. Date comparison: field.after(T(java.util.Date).valueOf('date')), field.before(...)
2001
+
2002
+ ## Output Format
2003
+ Output ONLY the SpEL expression. Do not include any explanation or comments.`;
2004
+ var SYSTEM_PROMPT_EN = `You are a Spring Expression Language (SpEL) expert. Convert natural language descriptions into precise SpEL expressions.
2005
+
2006
+ ## Rules
2007
+ 1. SpEL uses #variable for variables, #root.field for root object fields
2008
+ 2. String literals use single quotes: 'value'
2009
+ 3. Null comparison: == null or != null
2010
+ 4. Logical operators: and, or, not (NOT &&, ||, !)
2011
+ 5. Comparison operators: ==, !=, >, <, >=, <=
2012
+ 6. Elvis operator for defaults: value ?: default
2013
+ 7. Method calls: list.contains('item'), list.size(), list.isEmpty()
2014
+ 8. Collection projection: list.?[condition], list.^[condition], list.![projection]
2015
+ 9. Range check: value between {min, max}
2016
+ 10. Type check: variable instanceof T(ClassName)
2017
+ 11. Permission check: hasRole('role'), hasPermission('permission')
2018
+ 12. Date comparison: field.after(T(java.util.Date).valueOf('date')), field.before(...)
2019
+
2020
+ ## Output Format
2021
+ Output ONLY the SpEL expression. No explanation, no markdown.`;
2022
+ var SPEL_EBNF_GRAMMAR = `
2023
+ ## SpEL EBNF (simplified)
2024
+ expression := logicalOr
2025
+ logicalOr := logicalAnd ('or' logicalAnd)*
2026
+ logicalAnd := equality ('and' equality)*
2027
+ equality := relational (('==' | '!=') relational)?
2028
+ relational := additive (('>' | '<' | '>=' | '<=') additive)?
2029
+ additive := multiplicative (('+' | '-') multiplicative)*
2030
+ multiplicative := unary (('*' | '/' | '%') unary)*
2031
+ unary := ('not' | '!')? primary
2032
+ primary := literal | reference | method | '(' expression ')'
2033
+ | projection | selection | elvis | typeExpr
2034
+ literal := number | string | 'true' | 'false' | 'null'
2035
+ string := "'" (char)* "'"
2036
+ number := [0-9]+ ('.' [0-9]+)?
2037
+ reference := '#' identifier ('.' identifier)*
2038
+ method := reference '.'? identifier '(' args? ')'
2039
+ projection := reference '.[' projectionExpr ']'
2040
+ selection := reference '.?[' expr ']' | reference '.^[' expr ']'
2041
+ elvis := expression '?:' expression
2042
+ typeExpr := 'T(' qualifiedName ')'
2043
+ identifier := [a-zA-Z_][a-zA-Z0-9_]*
2044
+ `;
2045
+ var FEWSHOT_EXAMPLES = [
2046
+ // Easy (9 examples)
2047
+ {
2048
+ nl: "\u8BA2\u5355\u91D1\u989D\u5927\u4E8E1000",
2049
+ spel: "#order.amount > 1000",
2050
+ difficulty: "easy",
2051
+ category: "comparison"
2052
+ },
2053
+ { nl: "amount > 500", spel: "#order.amount > 500", difficulty: "easy", category: "comparison" },
2054
+ {
2055
+ nl: "\u8BA2\u5355\u72B6\u6001\u662F\u5DF2\u53D1\u8D27",
2056
+ spel: "#order.status == '\u5DF2\u53D1\u8D27'",
2057
+ difficulty: "easy",
2058
+ category: "comparison"
2059
+ },
2060
+ {
2061
+ nl: "status equals completed",
2062
+ spel: "#order.status == 'completed'",
2063
+ difficulty: "easy",
2064
+ category: "comparison"
2065
+ },
2066
+ { nl: "\u5907\u6CE8\u4E3A\u7A7A", spel: "#order.remark == null", difficulty: "easy", category: "null_check" },
2067
+ {
2068
+ nl: "remark is null",
2069
+ spel: "#order.remark == null",
2070
+ difficulty: "easy",
2071
+ category: "null_check"
2072
+ },
2073
+ {
2074
+ nl: "\u7528\u6237\u62E5\u6709\u7BA1\u7406\u5458\u89D2\u8272",
2075
+ spel: "hasRole('\u7BA1\u7406\u5458')",
2076
+ difficulty: "easy",
2077
+ category: "permission"
2078
+ },
2079
+ {
2080
+ nl: "user has admin role",
2081
+ spel: "hasRole('admin')",
2082
+ difficulty: "easy",
2083
+ category: "permission"
2084
+ },
2085
+ {
2086
+ nl: "\u6807\u7B7E\u5305\u542BVIP",
2087
+ spel: "#order.tags.contains('VIP')",
2088
+ difficulty: "easy",
2089
+ category: "collection"
2090
+ },
2091
+ // Medium (7 examples)
2092
+ {
2093
+ nl: "\u91D1\u989D\u5927\u4E8E100\u4E14\u8BA2\u5355\u5DF2\u786E\u8BA4",
2094
+ spel: "#order.amount > 100 and #order.status == '\u5DF2\u786E\u8BA4'",
2095
+ difficulty: "medium",
2096
+ category: "logical"
2097
+ },
2098
+ {
2099
+ nl: "amount > 100 and status == done",
2100
+ spel: "#order.amount > 100 and #order.status == 'done'",
2101
+ difficulty: "medium",
2102
+ category: "logical"
2103
+ },
2104
+ {
2105
+ nl: "\u91D1\u989D\u5927\u4E8E500\u6216\u5DF2\u53D1\u8D27",
2106
+ spel: "#order.amount > 500 or #order.status == '\u5DF2\u53D1\u8D27'",
2107
+ difficulty: "medium",
2108
+ category: "logical"
2109
+ },
2110
+ {
2111
+ nl: "\u5E74\u9F84\u572818\u523060\u4E4B\u95F4",
2112
+ spel: "#user.age between {18, 60}",
2113
+ difficulty: "medium",
2114
+ category: "range"
2115
+ },
2116
+ {
2117
+ nl: "amount between 100 and 500",
2118
+ spel: "#order.amount between {100, 500}",
2119
+ difficulty: "medium",
2120
+ category: "range"
2121
+ },
2122
+ {
2123
+ nl: "\u8BA2\u5355\u5907\u6CE8\u5305\u542B\u52A0\u6025",
2124
+ spel: "#order.remark.contains('\u52A0\u6025')",
2125
+ difficulty: "medium",
2126
+ category: "string"
2127
+ },
2128
+ {
2129
+ nl: "name contains test",
2130
+ spel: "#order.name.contains('test')",
2131
+ difficulty: "medium",
2132
+ category: "string"
2133
+ },
2134
+ // Hard (4 examples)
2135
+ {
2136
+ nl: "\u91D1\u989D\u5927\u4E8E1000\u4E14\u72B6\u6001\u4E3A\u5DF2\u53D1\u8D27\u4E14\u7528\u6237\u662FVIP",
2137
+ spel: "#order.amount > 1000 and #order.status == '\u5DF2\u53D1\u8D27' and #user.vip == true",
2138
+ difficulty: "hard",
2139
+ category: "complex_logical"
2140
+ },
2141
+ {
2142
+ nl: "\u7B5B\u9009\u51FA\u6240\u6709\u91D1\u989D\u5927\u4E8E1000\u7684\u8BA2\u5355",
2143
+ spel: "#order.items.?[#this.amount > 1000]",
2144
+ difficulty: "hard",
2145
+ category: "selection"
2146
+ },
2147
+ {
2148
+ nl: "amount > 500 and (status == done or status == shipped)",
2149
+ spel: "#order.amount > 500 and (#order.status == 'done' or #order.status == 'shipped')",
2150
+ difficulty: "hard",
2151
+ category: "complex_logical"
2152
+ },
2153
+ {
2154
+ nl: "\u8BA2\u5355\u4E2D\u7B2C\u4E00\u4E2A\u91D1\u989D\u5927\u4E8E1000\u7684\u5546\u54C1",
2155
+ spel: "#order.items.^[#this.amount > 1000]",
2156
+ difficulty: "hard",
2157
+ category: "selection"
2158
+ }
2159
+ ];
2160
+ var PromptBuilder = class {
2161
+ formatter = new SchemaFormatter();
2162
+ /**
2163
+ * Build a complete LLM Prompt
2164
+ */
2165
+ build(userInput, contextSchema, options = {}) {
2166
+ const lang = options.language ?? "zh";
2167
+ const includeEBNF = options.includeEBNF ?? true;
2168
+ const systemPrompt = lang === "zh" ? SYSTEM_PROMPT_ZH : SYSTEM_PROMPT_EN;
2169
+ const parts = [this.buildExamples(lang)];
2170
+ if (contextSchema && contextSchema.root) {
2171
+ parts.push(this.formatter.formatForLLM(contextSchema));
2172
+ }
2173
+ if (includeEBNF) {
2174
+ parts.push(SPEL_EBNF_GRAMMAR);
2175
+ }
2176
+ parts.push(`## User Input
2177
+ ${userInput}
2178
+
2179
+ ## SpEL Expression
2180
+ `);
2181
+ const user = parts.join("\n\n");
2182
+ return {
2183
+ system: systemPrompt,
2184
+ user,
2185
+ contextSchema: contextSchema ?? {
2186
+ root: null,
2187
+ variables: {},
2188
+ beans: {},
2189
+ types: {},
2190
+ functions: {}
2191
+ },
2192
+ examples: FEWSHOT_EXAMPLES
2193
+ };
2194
+ }
2195
+ /**
2196
+ * Build Few-Shot example string
2197
+ */
2198
+ buildExamples(lang) {
2199
+ const lines = [];
2200
+ const examples = lang === "zh" ? FEWSHOT_EXAMPLES : FEWSHOT_EXAMPLES.filter(
2201
+ (e) => (
2202
+ // Include all English examples (CJK characters indicate non-English)
2203
+ !/[\u4e00-\u9fff\u3400-\u4dbf]/.test(e.nl) || e.category === "comparison" || e.category === "logical"
2204
+ )
2205
+ );
2206
+ if (lang === "zh") {
2207
+ lines.push("## Examples (Few-Shot)");
2208
+ } else {
2209
+ lines.push("## Few-Shot Examples");
2210
+ }
2211
+ for (const example of examples) {
2212
+ lines.push(`NL: ${example.nl}`);
2213
+ lines.push(`SpEL: ${example.spel}`);
2214
+ lines.push("");
2215
+ }
2216
+ return lines.join("\n");
2217
+ }
2218
+ };
2219
+
2220
+ // src/validation/validation-pipeline.ts
2221
+ var ValidationPipeline = class {
2222
+ evaluator;
2223
+ constructor(evaluator = null) {
2224
+ this.evaluator = evaluator;
2225
+ }
2226
+ setEvaluator(evaluator) {
2227
+ this.evaluator = evaluator;
2228
+ }
2229
+ /**
2230
+ * Run the full validation pipeline
2231
+ */
2232
+ async validate(expression, contextSchema) {
2233
+ const errors = [];
2234
+ const warnings = [];
2235
+ const parseStage = await this.validateParse(expression);
2236
+ const typeStage = this.validateTypes(expression, contextSchema);
2237
+ const semanticStage = this.validateSemantic(expression);
2238
+ const contextStage = this.validateContext(expression, contextSchema);
2239
+ errors.push(
2240
+ ...parseStage.errors,
2241
+ ...typeStage.errors,
2242
+ ...semanticStage.errors,
2243
+ ...contextStage.errors
2244
+ );
2245
+ warnings.push(
2246
+ ...parseStage.warnings,
2247
+ ...typeStage.warnings,
2248
+ ...semanticStage.warnings,
2249
+ ...contextStage.warnings
2250
+ );
2251
+ return {
2252
+ valid: parseStage.passed && typeStage.passed && semanticStage.passed && contextStage.passed,
2253
+ stages: {
2254
+ parse: parseStage,
2255
+ type: typeStage,
2256
+ semantic: semanticStage,
2257
+ context: contextStage
2258
+ },
2259
+ errors,
2260
+ warnings
2261
+ };
2262
+ }
2263
+ /**
2264
+ * Stage 1: Parse Check — syntax validation
2265
+ */
2266
+ async validateParse(expression) {
2267
+ const errors = [];
2268
+ const warnings = [];
2269
+ if (!expression || expression.trim().length === 0) {
2270
+ errors.push({
2271
+ code: "PARSE-EMPTY",
2272
+ message: "Expression is empty",
2273
+ stage: "parse",
2274
+ requiresLLM: true
2275
+ });
2276
+ return { passed: false, errors, warnings };
2277
+ }
2278
+ if (!this.hasBalancedParentheses(expression)) {
2279
+ errors.push({
2280
+ code: "PARSE-UNBALANCED_PARENS",
2281
+ message: "Unbalanced parentheses in expression",
2282
+ stage: "parse",
2283
+ requiresLLM: true
2284
+ });
2285
+ }
2286
+ if (expression.includes("===") || expression.includes("!==")) {
2287
+ errors.push({
2288
+ code: "PARSE-JS_OPERATOR",
2289
+ message: "JavaScript operators detected (=== or !==), use == or != in SpEL",
2290
+ stage: "parse",
2291
+ requiresLLM: true
2292
+ });
2293
+ }
2294
+ if (expression.includes("&&")) {
2295
+ errors.push({
2296
+ code: "PARSE-JS_LOGIC",
2297
+ message: 'JavaScript && detected, use "and" in SpEL',
2298
+ stage: "parse",
2299
+ requiresLLM: true
2300
+ });
2301
+ }
2302
+ if (expression.includes("||")) {
2303
+ errors.push({
2304
+ code: "PARSE-JS_LOGIC",
2305
+ message: 'JavaScript || detected, use "or" in SpEL',
2306
+ stage: "parse",
2307
+ requiresLLM: true
2308
+ });
2309
+ }
2310
+ if (this.evaluator) {
2311
+ try {
2312
+ const parseResult = await this.evaluator.parse(expression);
2313
+ if (!parseResult.valid) {
2314
+ for (const pe of parseResult.errors) {
2315
+ errors.push({
2316
+ code: `PARSE-${pe.code ?? "SYNTAX"}`,
2317
+ message: pe.message,
2318
+ position: pe.position,
2319
+ stage: "parse",
2320
+ requiresLLM: true
2321
+ });
2322
+ }
2323
+ }
2324
+ } catch (err) {
2325
+ errors.push({
2326
+ code: "PARSE-EXCEPTION",
2327
+ message: `Parse threw exception: ${err.message}`,
2328
+ stage: "parse",
2329
+ requiresLLM: true
2330
+ });
2331
+ }
2332
+ }
2333
+ return {
2334
+ passed: errors.length === 0,
2335
+ errors,
2336
+ warnings
2337
+ };
2338
+ }
2339
+ /**
2340
+ * Stage 2: Type Check — type validation
2341
+ */
2342
+ validateTypes(expression, contextSchema) {
2343
+ const errors = [];
2344
+ const warnings = [];
2345
+ const strNumMismatch = /'(?:\\.|[^'\\])*'\s*(?:>|<|>=|<=)\s*\d+/;
2346
+ if (strNumMismatch.test(expression)) {
2347
+ warnings.push({
2348
+ code: "TYPE-STR_NUM_CMP",
2349
+ message: "String literal compared with number using arithmetic operator",
2350
+ stage: "type"
2351
+ });
2352
+ }
2353
+ if (contextSchema?.root) {
2354
+ for (const [fieldName, field] of Object.entries(contextSchema.root.fields)) {
2355
+ if (field.type === "boolean") {
2356
+ const boolNumPattern = new RegExp(`#\\w+\\.${fieldName}\\s*(?:>|<|>=|<=)\\s*\\d+`);
2357
+ if (boolNumPattern.test(expression)) {
2358
+ warnings.push({
2359
+ code: "TYPE-BOOL_NUM_CMP",
2360
+ message: `Boolean field '${fieldName}' compared with number`,
2361
+ stage: "type"
2362
+ });
2363
+ }
2364
+ }
2365
+ }
2366
+ }
2367
+ return {
2368
+ passed: errors.length === 0,
2369
+ errors,
2370
+ warnings
2371
+ };
2372
+ }
2373
+ /**
2374
+ * Stage 3: Semantic Check — semantic reasonableness validation
2375
+ */
2376
+ validateSemantic(expression) {
2377
+ const errors = [];
2378
+ const warnings = [];
2379
+ const selfCompare = /(#\w+(?:\.\w+)*)\s*==\s*\1/;
2380
+ if (selfCompare.test(expression)) {
2381
+ warnings.push({
2382
+ code: "SEM-SELF_COMPARE",
2383
+ message: "Self-comparison detected: expression is always true",
2384
+ stage: "semantic"
2385
+ });
2386
+ }
2387
+ if (expression.includes("!!")) {
2388
+ warnings.push({
2389
+ code: "SEM-DOUBLE_NEGATION",
2390
+ message: "Double negation detected, consider simplifying",
2391
+ stage: "semantic"
2392
+ });
2393
+ }
2394
+ return {
2395
+ passed: errors.length === 0,
2396
+ errors,
2397
+ warnings
2398
+ };
2399
+ }
2400
+ /**
2401
+ * Stage 4: Context Check — context reference validation
2402
+ */
2403
+ validateContext(expression, contextSchema) {
2404
+ const errors = [];
2405
+ const warnings = [];
2406
+ if (!contextSchema) {
2407
+ warnings.push({
2408
+ code: "CTX-NO_SCHEMA",
2409
+ message: "No ContextSchema provided, skipping context validation",
2410
+ stage: "context"
2411
+ });
2412
+ return { passed: true, errors, warnings };
2413
+ }
2414
+ const refs = this.extractReferences(expression);
2415
+ if (contextSchema.root) {
2416
+ const rootName = contextSchema.root.name;
2417
+ for (const ref of refs) {
2418
+ if (ref.startsWith(`#${rootName}`)) {
2419
+ const parts = ref.split(".");
2420
+ const field = parts[1];
2421
+ if (field && !(field in contextSchema.root.fields)) {
2422
+ warnings.push({
2423
+ code: "CTX-UNKNOWN_FIELD",
2424
+ message: `Field '${field}' not found in root '${rootName}'`,
2425
+ stage: "context"
2426
+ });
2427
+ }
2428
+ }
2429
+ }
2430
+ }
2431
+ for (const ref of refs) {
2432
+ if (ref.startsWith("#") && !ref.includes(".")) {
2433
+ const varName = ref.slice(1);
2434
+ const isRoot = contextSchema.root?.name === varName;
2435
+ const isVariable = varName in contextSchema.variables;
2436
+ const isFunction = varName in contextSchema.functions;
2437
+ if (!isRoot && !isVariable && !isFunction) {
2438
+ warnings.push({
2439
+ code: "CTX-UNKNOWN_REF",
2440
+ message: `Unknown reference '${ref}'`,
2441
+ stage: "context"
2442
+ });
2443
+ }
2444
+ }
2445
+ }
2446
+ const beanMatch = expression.match(/@(\w+)/g);
2447
+ if (beanMatch && contextSchema.beans) {
2448
+ for (const b of beanMatch) {
2449
+ const beanName = b.slice(1);
2450
+ if (!(beanName in contextSchema.beans)) {
2451
+ warnings.push({
2452
+ code: "CTX-UNKNOWN_BEAN",
2453
+ message: `Bean '${beanName}' not found in ContextSchema`,
2454
+ stage: "context"
2455
+ });
2456
+ }
2457
+ }
2458
+ }
2459
+ return {
2460
+ passed: errors.length === 0,
2461
+ errors,
2462
+ warnings
2463
+ };
2464
+ }
2465
+ /**
2466
+ * Check if parentheses are balanced
2467
+ */
2468
+ hasBalancedParentheses(expression) {
2469
+ const stack = [];
2470
+ const pairs = { "(": ")", "[": "]", "{": "}" };
2471
+ for (const ch of expression) {
2472
+ if (ch in pairs) {
2473
+ stack.push(ch);
2474
+ } else if (ch === ")" || ch === "]" || ch === "}") {
2475
+ const last = stack.pop();
2476
+ if (!last || pairs[last] !== ch) return false;
2477
+ }
2478
+ }
2479
+ return stack.length === 0;
2480
+ }
2481
+ /**
2482
+ * Extract all identifier references from expression
2483
+ */
2484
+ extractReferences(expression) {
2485
+ const refs = [];
2486
+ const varMatch = expression.matchAll(/#(\w+(?:\.\w+(?:\.\w+)?)?)/g);
2487
+ for (const m of varMatch) {
2488
+ refs.push(`#${m[1]}`);
2489
+ }
2490
+ const simpleMatch = expression.matchAll(/#(\w+)(?!\w*\()/g);
2491
+ for (const m of simpleMatch) {
2492
+ const ref = `#${m[1]}`;
2493
+ if (!refs.includes(ref)) {
2494
+ refs.push(ref);
2495
+ }
2496
+ }
2497
+ return refs;
2498
+ }
2499
+ };
2500
+
2501
+ // src/validation/auto-fixer.ts
2502
+ var AutoFixer = class {
2503
+ fix(expression) {
2504
+ let fixed = expression;
2505
+ const changes = [];
2506
+ if (fixed.includes("=== undefined")) {
2507
+ fixed = fixed.replace(/=== undefined/g, "== null");
2508
+ changes.push("Replaced === undefined with == null");
2509
+ }
2510
+ if (fixed.includes("!== undefined")) {
2511
+ fixed = fixed.replace(/!== undefined/g, "!= null");
2512
+ changes.push("Replaced !== undefined with != null");
2513
+ }
2514
+ if (fixed.includes("!==")) {
2515
+ const count = fixed.match(/!==/g).length;
2516
+ fixed = fixed.replace(/!==/g, "!=");
2517
+ changes.push(`Replaced ${count}x !== with !=`);
2518
+ }
2519
+ if (fixed.includes("===")) {
2520
+ const count = fixed.match(/===/g).length;
2521
+ fixed = fixed.replace(/===/g, "==");
2522
+ changes.push(`Replaced ${count}x === with ==`);
2523
+ }
2524
+ if (fixed.includes("&&")) {
2525
+ const count = fixed.match(/&&/g).length;
2526
+ fixed = fixed.replace(/&&/g, "and");
2527
+ changes.push(`Replaced ${count}x && with and`);
2528
+ }
2529
+ if (fixed.includes("||")) {
2530
+ const count = fixed.match(/\|\|/g).length;
2531
+ fixed = fixed.replace(/\|\|/g, "or");
2532
+ changes.push(`Replaced ${count}x || with or`);
2533
+ }
2534
+ const singleQuoteCount = (fixed.match(/'/g) ?? []).length;
2535
+ if (singleQuoteCount % 2 !== 0) {
2536
+ if (!fixed.endsWith("'")) {
2537
+ fixed = fixed + "'";
2538
+ changes.push("Added missing closing single quote");
2539
+ }
2540
+ }
2541
+ const doubleQuoteCount = (fixed.match(/"/g) ?? []).length;
2542
+ if (doubleQuoteCount % 2 !== 0) {
2543
+ if (!fixed.endsWith('"')) {
2544
+ fixed = fixed + '"';
2545
+ changes.push("Added missing closing double quote");
2546
+ }
2547
+ }
2548
+ const parenFix = this.fixAllBrackets(fixed);
2549
+ if (parenFix !== fixed) {
2550
+ changes.push("Fixed unbalanced brackets");
2551
+ fixed = parenFix;
2552
+ }
2553
+ if (fixed.includes("> ==")) {
2554
+ fixed = fixed.replace(/> ==/g, ">=");
2555
+ changes.push("Replaced > == with >=");
2556
+ }
2557
+ if (fixed.includes("< ==")) {
2558
+ fixed = fixed.replace(/< ==/g, "<=");
2559
+ changes.push("Replaced < == with <=");
2560
+ }
2561
+ if (fixed.includes("? :")) {
2562
+ fixed = fixed.replace(/\?\s*:\s*/g, " ?: ");
2563
+ changes.push("Fixed Elvis operator spacing");
2564
+ }
2565
+ const wasFixed = changes.length > 0;
2566
+ return {
2567
+ wasFixed,
2568
+ expression: wasFixed ? fixed : expression,
2569
+ changes
2570
+ };
2571
+ }
2572
+ fixAllBrackets(expr) {
2573
+ let result = expr;
2574
+ const openParen = (result.match(/\(/g) ?? []).length;
2575
+ const closeParen = (result.match(/\)/g) ?? []).length;
2576
+ if (openParen > closeParen) {
2577
+ result += ")".repeat(openParen - closeParen);
2578
+ }
2579
+ const openBracket = (result.match(/\[/g) ?? []).length;
2580
+ const closeBracket = (result.match(/\]/g) ?? []).length;
2581
+ if (openBracket > closeBracket) {
2582
+ result += "]".repeat(openBracket - closeBracket);
2583
+ }
2584
+ const openBrace = (result.match(/\{/g) ?? []).length;
2585
+ const closeBrace = (result.match(/\}/g) ?? []).length;
2586
+ if (openBrace > closeBrace) {
2587
+ result += "}".repeat(openBrace - closeBrace);
2588
+ }
2589
+ return result;
2590
+ }
2591
+ };
2592
+
2593
+ // src/validation/self-correction-loop.ts
2594
+ var SelfCorrectionLoop = class {
2595
+ pipeline;
2596
+ autoFixer;
2597
+ config;
2598
+ constructor(config = {}, pipeline, autoFixer) {
2599
+ this.config = {
2600
+ maxAttempts: config.maxAttempts ?? 3,
2601
+ minConfidence: config.minConfidence ?? 0.7,
2602
+ enableAutoFix: config.enableAutoFix ?? true
2603
+ };
2604
+ this.pipeline = pipeline ?? new ValidationPipeline();
2605
+ this.autoFixer = autoFixer ?? new AutoFixer();
2606
+ }
2607
+ /**
2608
+ * Execute the self-correction loop
2609
+ */
2610
+ async correct(expression, contextSchema, generateFn, originalPrompt) {
2611
+ const startTime = Date.now();
2612
+ const corrections = [];
2613
+ let currentExpression = expression;
2614
+ let valid = false;
2615
+ const initialResult = await this.pipeline.validate(currentExpression, contextSchema);
2616
+ if (initialResult.valid) {
2617
+ return {
2618
+ expression: currentExpression,
2619
+ valid: true,
2620
+ originalOutput: expression,
2621
+ correctionAttempts: 0,
2622
+ corrections,
2623
+ totalLatencyMs: Date.now() - startTime
2624
+ };
2625
+ }
2626
+ if (this.config.enableAutoFix && initialResult.errors.length > 0) {
2627
+ const autoFixResult = this.autoFixer.fix(currentExpression);
2628
+ if (autoFixResult.wasFixed) {
2629
+ currentExpression = autoFixResult.expression;
2630
+ const afterFix = await this.pipeline.validate(currentExpression, contextSchema);
2631
+ corrections.push({
2632
+ attempt: 0,
2633
+ expression: currentExpression,
2634
+ valid: afterFix.valid,
2635
+ errorCount: afterFix.errors.length,
2636
+ warningCount: afterFix.warnings.length,
2637
+ autoFixed: true,
2638
+ autoFixChanges: autoFixResult.changes
2639
+ });
2640
+ if (afterFix.valid) {
2641
+ return {
2642
+ expression: currentExpression,
2643
+ valid: true,
2644
+ originalOutput: expression,
2645
+ correctionAttempts: 1,
2646
+ corrections,
2647
+ totalLatencyMs: Date.now() - startTime
2648
+ };
2649
+ }
2650
+ }
2651
+ }
2652
+ for (let attempt = 1; attempt <= this.config.maxAttempts; attempt++) {
2653
+ const lastValidation = await this.pipeline.validate(currentExpression, contextSchema);
2654
+ const errorDescriptions = lastValidation.errors.map((e) => `- [${e.code}] ${e.message}`).join("\n");
2655
+ const correctionPrompt = {
2656
+ ...originalPrompt,
2657
+ user: [
2658
+ originalPrompt.user,
2659
+ "",
2660
+ "---",
2661
+ "The previous generated expression was INVALID. Please fix it:",
2662
+ `Expression: ${currentExpression}`,
2663
+ `Errors:
2664
+ ${errorDescriptions}`,
2665
+ "---",
2666
+ "Generate ONLY the corrected SpEL expression, nothing else."
2667
+ ].join("\n")
2668
+ };
2669
+ try {
2670
+ const response = await generateFn(correctionPrompt);
2671
+ currentExpression = response.text.trim();
2672
+ if (this.config.enableAutoFix) {
2673
+ const afResult = this.autoFixer.fix(currentExpression);
2674
+ if (afResult.wasFixed) {
2675
+ currentExpression = afResult.expression;
2676
+ }
2677
+ }
2678
+ const currentValidation = await this.pipeline.validate(currentExpression, contextSchema);
2679
+ corrections.push({
2680
+ attempt,
2681
+ expression: currentExpression,
2682
+ valid: currentValidation.valid,
2683
+ errorCount: currentValidation.errors.length,
2684
+ warningCount: currentValidation.warnings.length,
2685
+ autoFixed: false,
2686
+ autoFixChanges: []
2687
+ });
2688
+ if (currentValidation.valid) {
2689
+ valid = true;
2690
+ break;
2691
+ }
2692
+ } catch (err) {
2693
+ corrections.push({
2694
+ attempt,
2695
+ expression: currentExpression,
2696
+ valid: false,
2697
+ errorCount: 1,
2698
+ warningCount: 0,
2699
+ autoFixed: false,
2700
+ autoFixChanges: []
2701
+ });
2702
+ }
2703
+ }
2704
+ return {
2705
+ expression: currentExpression,
2706
+ valid,
2707
+ originalOutput: expression,
2708
+ correctionAttempts: corrections.filter((c) => c.attempt > 0).length,
2709
+ corrections,
2710
+ totalLatencyMs: Date.now() - startTime
2711
+ };
2712
+ }
2713
+ };
2714
+
2715
+ // src/strategy/strategy-router.ts
2716
+ var StrategyRouter = class {
2717
+ patternMatcher;
2718
+ intentClassifier;
2719
+ templateEngine;
2720
+ promptBuilder;
2721
+ validationPipeline;
2722
+ autoFixer;
2723
+ providerRegistry;
2724
+ config;
2725
+ constructor(providerRegistry, config = {}) {
2726
+ this.patternMatcher = new PatternMatcher(BUILTIN_PATTERNS);
2727
+ this.intentClassifier = new IntentClassifier();
2728
+ this.templateEngine = new TemplateEngine();
2729
+ this.promptBuilder = new PromptBuilder();
2730
+ this.validationPipeline = new ValidationPipeline();
2731
+ this.autoFixer = new AutoFixer();
2732
+ this.providerRegistry = providerRegistry;
2733
+ this.config = {
2734
+ patternMinConfidence: config.patternMinConfidence ?? 0.7,
2735
+ templateMinConfidence: config.templateMinConfidence ?? 0.6,
2736
+ llmMinConfidence: config.llmMinConfidence ?? 0.5,
2737
+ enableSelfCorrection: config.enableSelfCorrection ?? true,
2738
+ maxCorrectionAttempts: config.maxCorrectionAttempts ?? 3
2739
+ };
2740
+ }
2741
+ /**
2742
+ * Execute the full generation strategy routing
2743
+ */
2744
+ async generate(nl, contextSchema, forceLLMProvider) {
2745
+ const startTime = Date.now();
2746
+ if (contextSchema) {
2747
+ this.templateEngine.setContext(contextSchema);
2748
+ }
2749
+ const patternResult = this.patternMatcher.match(nl);
2750
+ if (patternResult.matched && patternResult.confidence >= this.config.patternMinConfidence) {
2751
+ const validation = await this.validationPipeline.validate(patternResult.spel, contextSchema);
2752
+ if (validation.valid) {
2753
+ return {
2754
+ expression: patternResult.spel,
2755
+ strategy: "pattern",
2756
+ confidence: patternResult.confidence,
2757
+ metadata: { patternId: patternResult.pattern?.id },
2758
+ latencyMs: Date.now() - startTime
2759
+ };
2760
+ }
2761
+ try {
2762
+ const afResult = this.autoFixer.fix(patternResult.spel);
2763
+ if (afResult.wasFixed) {
2764
+ const afValidation = await this.validationPipeline.validate(
2765
+ afResult.expression,
2766
+ contextSchema
2767
+ );
2768
+ if (afValidation.valid) {
2769
+ return {
2770
+ expression: afResult.expression,
2771
+ strategy: "pattern",
2772
+ confidence: patternResult.confidence * 0.95,
2773
+ metadata: { patternId: patternResult.pattern?.id },
2774
+ latencyMs: Date.now() - startTime
2775
+ };
2776
+ }
2777
+ }
2778
+ } catch {
2779
+ }
2780
+ }
2781
+ const intentResult = this.intentClassifier.classify(nl);
2782
+ let templateResult = null;
2783
+ try {
2784
+ templateResult = this.templateEngine.generate(nl, intentResult);
2785
+ } catch {
2786
+ }
2787
+ if (templateResult && templateResult.confidence >= this.config.templateMinConfidence && templateResult.unfilledSlots.length === 0) {
2788
+ const validation = await this.validationPipeline.validate(
2789
+ templateResult.expression,
2790
+ contextSchema
2791
+ );
2792
+ if (validation.valid) {
2793
+ return {
2794
+ expression: templateResult.expression,
2795
+ strategy: "template",
2796
+ confidence: templateResult.confidence,
2797
+ metadata: {
2798
+ intent: templateResult.intent,
2799
+ templateName: templateResult.templateName
2800
+ },
2801
+ latencyMs: Date.now() - startTime
2802
+ };
2803
+ }
2804
+ }
2805
+ const prompt = this.promptBuilder.build(nl, contextSchema);
2806
+ let providers = await this.providerRegistry.getPrioritized();
2807
+ if (forceLLMProvider) {
2808
+ const forced = providers.find((p) => p.name === forceLLMProvider);
2809
+ if (forced) {
2810
+ providers = [forced, ...providers.filter((p) => p !== forced)];
2811
+ }
2812
+ }
2813
+ if (providers.length === 0) {
2814
+ throw new Error("No LLM providers available");
2815
+ }
2816
+ let lastError = null;
2817
+ for (const provider of providers) {
2818
+ try {
2819
+ const response = await provider.generate(prompt, { timeout: 3e4 });
2820
+ let expression = response.text.trim();
2821
+ let corrections = 0;
2822
+ if (this.config.enableSelfCorrection && contextSchema) {
2823
+ const correctionLoop = new SelfCorrectionLoop(
2824
+ {
2825
+ maxAttempts: this.config.maxCorrectionAttempts
2826
+ },
2827
+ this.validationPipeline
2828
+ );
2829
+ const correctionResult = await correctionLoop.correct(
2830
+ expression,
2831
+ contextSchema,
2832
+ (prompt2) => provider.generate(prompt2),
2833
+ prompt
2834
+ );
2835
+ expression = correctionResult.expression;
2836
+ corrections = correctionResult.correctionAttempts;
2837
+ }
2838
+ return {
2839
+ expression,
2840
+ strategy: "llm-api",
2841
+ confidence: 0.8,
2842
+ metadata: {
2843
+ providerName: provider.name,
2844
+ model: response.model,
2845
+ corrections,
2846
+ rawOutput: response.text
2847
+ },
2848
+ latencyMs: Date.now() - startTime
2849
+ };
2850
+ } catch (err) {
2851
+ lastError = err;
2852
+ continue;
2853
+ }
2854
+ }
2855
+ throw new Error(`All LLM providers failed. Last error: ${lastError?.message ?? "Unknown"}`);
2856
+ }
2857
+ /**
2858
+ * Get PatternMatcher (for external testing/debugging)
2859
+ */
2860
+ getPatternMatcher() {
2861
+ return this.patternMatcher;
2862
+ }
2863
+ /**
2864
+ * Get TemplateEngine
2865
+ */
2866
+ getTemplateEngine() {
2867
+ return this.templateEngine;
2868
+ }
2869
+ /**
2870
+ * Get PromptBuilder
2871
+ */
2872
+ getPromptBuilder() {
2873
+ return this.promptBuilder;
2874
+ }
2875
+ /**
2876
+ * Set SpelEvaluator for validation pipeline.
2877
+ * Must be called before generate() if parse-level validation is needed.
2878
+ */
2879
+ setEvaluator(evaluator) {
2880
+ this.validationPipeline.setEvaluator(evaluator);
2881
+ }
2882
+ };
2883
+
2884
+ // src/engine/nl2spel-engine.ts
2885
+ var NL2SpelEngine = class {
2886
+ router;
2887
+ providerRegistry;
2888
+ evaluatorInstance = null;
2889
+ constructor(config = {}) {
2890
+ this.providerRegistry = new ProviderRegistry();
2891
+ this.router = new StrategyRouter(this.providerRegistry, config);
2892
+ }
2893
+ /**
2894
+ * Register an LLM Provider
2895
+ */
2896
+ registerProvider(provider) {
2897
+ this.providerRegistry.register(provider);
2898
+ }
2899
+ /**
2900
+ * Unregister an LLM Provider
2901
+ */
2902
+ unregisterProvider(name) {
2903
+ this.providerRegistry.unregister(name);
2904
+ }
2905
+ /**
2906
+ * Register a custom Pattern
2907
+ */
2908
+ registerPattern(pattern) {
2909
+ this.router.getPatternMatcher().register(pattern);
2910
+ }
2911
+ /**
2912
+ * Set SpelEvaluator for validation pipeline.
2913
+ * Must be called before generate() if parse-level validation is required.
2914
+ */
2915
+ setSpelEvaluator(evaluator) {
2916
+ this.evaluatorInstance = evaluator;
2917
+ this.router.setEvaluator(evaluator);
2918
+ }
2919
+ /**
2920
+ * Extract ContextSchema from raw context object
2921
+ */
2922
+ extractContextSchema(context) {
2923
+ const extractor = new ContextExtractor();
2924
+ return extractor.extract(context);
2925
+ }
2926
+ /**
2927
+ * Core generation API
2928
+ */
2929
+ async generate(nl, options = {}) {
2930
+ const startTime = Date.now();
2931
+ let contextSchema = options.contextSchema;
2932
+ if (!contextSchema && this.evaluatorInstance) {
2933
+ const schema = await Promise.resolve(this.evaluatorInstance.getContextSchema());
2934
+ if (schema) contextSchema = schema;
2935
+ }
2936
+ if (!contextSchema && options.context) {
2937
+ const extractor = new ContextExtractor();
2938
+ contextSchema = extractor.extract(options.context);
2939
+ }
2940
+ if (options.offlineOnly) {
2941
+ const patternMatcher = this.router.getPatternMatcher();
2942
+ const patternResult = patternMatcher.match(nl);
2943
+ if (patternResult.matched) {
2944
+ return {
2945
+ expression: patternResult.spel,
2946
+ strategy: "pattern",
2947
+ confidence: patternResult.confidence,
2948
+ latencyMs: Date.now() - startTime
2949
+ };
2950
+ }
2951
+ const classifier = new IntentClassifier();
2952
+ const intentResult = classifier.classify(nl);
2953
+ const templateEngine = this.router.getTemplateEngine();
2954
+ if (contextSchema) templateEngine.setContext(contextSchema);
2955
+ const templateResult = templateEngine.generate(nl, intentResult);
2956
+ if (templateResult) {
2957
+ return {
2958
+ expression: templateResult.expression,
2959
+ strategy: "template",
2960
+ confidence: templateResult.confidence,
2961
+ latencyMs: Date.now() - startTime
2962
+ };
2963
+ }
2964
+ throw new Error("Cannot generate expression offline: no pattern or template matched.");
2965
+ }
2966
+ const result = await this.router.generate(nl, contextSchema, options.preferredProvider);
2967
+ return {
2968
+ expression: result.expression,
2969
+ strategy: result.strategy,
2970
+ confidence: result.confidence,
2971
+ latencyMs: Date.now() - startTime
2972
+ };
2973
+ }
2974
+ /**
2975
+ * Batch generation
2976
+ */
2977
+ async generateBatch(nls, options = {}) {
2978
+ return Promise.all(nls.map((nl) => this.generate(nl, options)));
2979
+ }
2980
+ /**
2981
+ * Debug explanation
2982
+ */
2983
+ async explain(nl, options = {}) {
2984
+ const classifier = new IntentClassifier();
2985
+ const intentResult = classifier.classify(nl);
2986
+ const patternMatcher = this.router.getPatternMatcher();
2987
+ const patternResult = patternMatcher.match(nl);
2988
+ const allPatternResults = patternMatcher.matchAll(nl, 5);
2989
+ const result = await this.generate(nl, { ...options });
2990
+ return {
2991
+ input: nl,
2992
+ strategy: result.strategy,
2993
+ intent: {
2994
+ primary: intentResult.primaryIntent,
2995
+ complexity: intentResult.complexity,
2996
+ all: intentResult.intents
2997
+ },
2998
+ patternMatched: patternResult.matched,
2999
+ patternId: patternResult.pattern?.id,
3000
+ expression: result.expression,
3001
+ alternatives: allPatternResults.filter((r) => r.spel).map((r) => r.spel)
3002
+ };
3003
+ }
3004
+ };
3005
+ export {
3006
+ AutoFixer,
3007
+ BUILTIN_PATTERNS,
3008
+ ChineseNumberParser,
3009
+ ContextExtractor,
3010
+ IntentClassifier,
3011
+ NL2SpelEngine,
3012
+ NLIntent,
3013
+ PatternMatcher,
3014
+ PromptBuilder,
3015
+ ProviderRegistry,
3016
+ SchemaFormatter,
3017
+ SelfCorrectionLoop,
3018
+ StrategyRouter,
3019
+ TemplateEngine,
3020
+ ValidationPipeline
3021
+ };