@agentix-e/nl2spel 1.2.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -38,6 +38,9 @@ interface LLMProvider {
38
38
  }
39
39
  /**
40
40
  * LLMProvider capability declaration.
41
+ *
42
+ * Declares what the provider CAN do — facts the engine can verify.
43
+ * Provider ordering is user-controlled via {@link ProviderRegistry.register} priority.
41
44
  */
42
45
  interface LLMCapabilities {
43
46
  /** Maximum context window (tokens) */
@@ -48,14 +51,8 @@ interface LLMCapabilities {
48
51
  supportsStreaming: boolean;
49
52
  /** Whether Structured Output (JSON mode) is supported */
50
53
  supportsStructuredOutput: boolean;
51
- /** Whether available offline */
54
+ /** Whether available offline (no network required) */
52
55
  offlineAvailable: boolean;
53
- /**
54
- * Estimated cost per request (USD)
55
- */
56
- estimatedCostPerRequest?: number;
57
- /** Estimated latency (ms) */
58
- estimatedLatencyMs: number;
59
56
  }
60
57
  /**
61
58
  * Standardized LLM Prompt structure.
@@ -148,24 +145,36 @@ interface LLMUsage {
148
145
  /**
149
146
  * ProviderRegistry — manages registered LLMProvider instances.
150
147
  *
151
- * Responsibilities:
152
- * 1. Register/unregister Providers
153
- * 2. Sort by priority (offline first > low latency > high accuracy)
154
- * 3. Look up Providers by name (for forced selection)
148
+ * Provider ordering is user-controlled:
149
+ * 1. Offline providers first (engine-enforced — offline capability is a binary fact)
150
+ * 2. User-assigned priority (lower = preferred; default = registration order)
151
+ * 3. Registration order (tiebreaker when priorities are equal)
155
152
  */
156
153
  declare class ProviderRegistry {
157
154
  private _providers;
158
- /** Register a Provider */
159
- register(provider: LLMProvider): void;
155
+ private _nextIndex;
156
+ /**
157
+ * Register a Provider.
158
+ * @param provider LLMProvider instance
159
+ * @param options.priority User-assigned priority (lower = preferred). Defaults to registration order.
160
+ */
161
+ register(provider: LLMProvider, options?: {
162
+ priority?: number;
163
+ }): void;
160
164
  /** Unregister a Provider */
161
165
  unregister(name: string): void;
162
166
  /** Get a Provider by name */
163
167
  get(name: string): LLMProvider | undefined;
164
168
  /**
165
169
  * Get available Providers sorted by priority.
166
- * Sort rule: offline > low cost > low latency > high accuracy
170
+ * Sort rule: offline first user priority (asc) registration order (asc)
167
171
  */
168
172
  getPrioritized(): Promise<LLMProvider[]>;
173
+ /**
174
+ * Explicitly reorder providers by name.
175
+ * Providers not listed retain their position after the reordered ones.
176
+ */
177
+ reorder(providerNames: string[]): void;
169
178
  /** List all registered Providers */
170
179
  list(): LLMProvider[];
171
180
  /** Number of registered Providers */
@@ -325,6 +334,25 @@ interface SlotDefinition {
325
334
  }
326
335
  type SlotTransform = 'toNumber' | 'toBoolean' | 'toString' | 'trim' | 'lowercase' | 'normalize';
327
336
 
337
+ /**
338
+ * How to treat a field word the dictionary does not know.
339
+ *
340
+ * `passthrough` emits the word verbatim, which is legal Spring — `Character.isLetter`
341
+ * accepts any Unicode letter — and is the default, because a caller whose schema
342
+ * uses Chinese property names depends on it. `strict` refuses instead, for callers
343
+ * who would rather see an error than an unresolved name.
344
+ */
345
+ type FieldPolicy = 'passthrough' | 'strict';
346
+ /** Thrown under `strict` when a field word has no dictionary entry. */
347
+ declare class UnmappedFieldError extends Error {
348
+ /** The field word that could not be resolved. */
349
+ readonly field: string;
350
+ constructor(field: string);
351
+ }
352
+ interface PatternMatcherOptions {
353
+ /** How to treat a field word with no dictionary entry. Default `passthrough`. */
354
+ fieldPolicy?: FieldPolicy;
355
+ }
328
356
  interface PatternMatchResult {
329
357
  /** Whether matched */
330
358
  matched: boolean;
@@ -338,13 +366,17 @@ interface PatternMatchResult {
338
366
  latencyMs: number;
339
367
  /** Extracted slot values */
340
368
  slots?: Record<string, string>;
369
+ /**
370
+ * Field words emitted verbatim because the dictionary had no entry, in
371
+ * encounter order. Populated under either policy, so a caller can always see
372
+ * what was not resolved rather than having to infer it.
373
+ */
374
+ unmappedFields?: string[];
341
375
  }
342
- /**
343
- * PatternMatcher — Layer 0 pattern matching core.
344
- */
345
376
  declare class PatternMatcher {
346
377
  private _patterns;
347
- constructor(patterns?: PatternDefinition[]);
378
+ private readonly fieldPolicy;
379
+ constructor(patterns?: PatternDefinition[], options?: PatternMatcherOptions);
348
380
  get patternCount(): number;
349
381
  register(pattern: PatternDefinition): void;
350
382
  registerAll(patterns: PatternDefinition[]): void;
@@ -365,18 +397,29 @@ declare class PatternMatcher {
365
397
  * Infer SpEL root object name from Chinese input
366
398
  */
367
399
  private inferRoot;
400
+ /**
401
+ * The first whitespace- or punctuation-delimited chunk of `input`.
402
+ */
403
+ private firstWord;
368
404
  /**
369
405
  * Extract Chinese field names from input and map to SpEL fields
370
406
  */
371
407
  private extractChineseField;
372
408
  /**
373
- * Template filling and value transformation
409
+ * Resolve a captured field word into the identifier to emit, and report whether
410
+ * the dictionary recognised it.
411
+ *
412
+ * An ASCII word is already an identifier and is emitted unchanged. Anything else
413
+ * is looked up: a word the dictionary knows becomes its English identifier, and a
414
+ * word it does not know is emitted verbatim and reported as unmapped. Emitting it
415
+ * verbatim is legal Spring, but it is a guess about the caller's schema, so the
416
+ * guess is never silent.
374
417
  */
375
- private fillTemplate;
418
+ private resolveField;
376
419
  /**
377
- * Infer SpEL field name from capture group
420
+ * Template filling and value transformation
378
421
  */
379
- private inferFieldFromCapture;
422
+ private fillTemplate;
380
423
  }
381
424
 
382
425
  /**
@@ -390,6 +433,60 @@ declare class PatternMatcher {
390
433
  */
391
434
  declare const BUILTIN_PATTERNS: PatternDefinition[];
392
435
 
436
+ /**
437
+ * Clause decomposition for compound natural-language rules.
438
+ *
439
+ * A single comparison pattern matches a prefix and ignores the rest, so a
440
+ * sentence that joins two conditions is silently answered with only the first
441
+ * one: `金额大于1000且订单已确认` used to produce `#amount > 1000`, dropping the
442
+ * confirmation requirement entirely. The result parses, so nothing downstream
443
+ * notices — a materially weaker rule than the one that was asked for.
444
+ *
445
+ * This module splits such a sentence on its top-level logical connectors, has the
446
+ * caller convert each clause independently, and joins the results with SpEL's
447
+ * `and`/`or`. A clause that cannot be converted is reported rather than dropped:
448
+ * a partial rule is never emitted.
449
+ */
450
+ interface Clause {
451
+ /** Clause text, trimmed. */
452
+ text: string;
453
+ /**
454
+ * How this clause attaches to the previous one. The first clause carries
455
+ * `'and'`, which is never used because a single clause needs no join.
456
+ */
457
+ connector: 'and' | 'or';
458
+ }
459
+ /** Raised when a compound sentence contains a clause that cannot be converted. */
460
+ declare class UnconvertibleClauseError extends Error {
461
+ /** The clause texts that could not be converted. */
462
+ readonly unconvertible: readonly string[];
463
+ /** The full input that was being decomposed. */
464
+ readonly input: string;
465
+ constructor(input: string, unconvertible: readonly string[]);
466
+ }
467
+ /**
468
+ * Split `input` on its top-level logical connectors.
469
+ *
470
+ * Returns a single clause when there is no top-level connector, so the caller can
471
+ * treat "one clause" and "no decomposition needed" identically.
472
+ */
473
+ declare function splitClauses(input: string): Clause[];
474
+ interface Decomposition {
475
+ /** The joined expression, with every operand parenthesised. */
476
+ expression: string;
477
+ /** The clause texts, in order. */
478
+ clauses: string[];
479
+ }
480
+ /**
481
+ * Decompose `input` and convert every clause with `convert`.
482
+ *
483
+ * Returns `null` when the input has no top-level connector — the caller should
484
+ * run its ordinary single-pass conversion instead. Throws
485
+ * {@link UnconvertibleClauseError} when any clause cannot be converted, so a
486
+ * partially understood sentence is never answered with a partial rule.
487
+ */
488
+ declare function decompose(input: string, convert: (clause: string) => string | null): Decomposition | null;
489
+
393
490
  /**
394
491
  * NLIntent — Natural language intent classification system.
395
492
  *
@@ -505,6 +602,22 @@ declare class TemplateEngine {
505
602
  generate(input: string, intentResult: IntentResult): TemplateResult | null;
506
603
  private selectBestTemplate;
507
604
  private fillTemplate;
605
+ /**
606
+ * Resolve the SpEL root name for `input`: the configured schema root when one
607
+ * exists, otherwise a keyword heuristic over the well-known roots.
608
+ */
609
+ private resolveRootName;
610
+ /**
611
+ * Resolve the field name for `input`.
612
+ *
613
+ * The previous implementation only compared schema *keys* and otherwise left
614
+ * the literal placeholder default in place, which is how "#order.field == null"
615
+ * reached callers. The lookup now falls back in order: schema key, schema
616
+ * field description (Chinese inputs name the field by its description),
617
+ * Chinese field word, first English identifier, and finally "value" — the
618
+ * neutral default the pattern layer already uses for an unknown field.
619
+ */
620
+ private resolveFieldName;
508
621
  }
509
622
 
510
623
  interface PromptBuilderOptions {
@@ -525,6 +638,10 @@ declare class PromptBuilder {
525
638
  private buildExamples;
526
639
  }
527
640
 
641
+ /** Stage a diagnostic was produced by. */
642
+ type ValidationStage = 'parse' | 'type' | 'semantic' | 'context' | 'final';
643
+ /** Whether a diagnostic rejects the expression or merely observes it. */
644
+ type ValidationSeverity = 'error' | 'warning';
528
645
  interface ValidationResult {
529
646
  /** Whether all validation stages passed */
530
647
  valid: boolean;
@@ -534,6 +651,7 @@ interface ValidationResult {
534
651
  type: StageResult;
535
652
  semantic: StageResult;
536
653
  context: StageResult;
654
+ final: StageResult;
537
655
  };
538
656
  /** All errors */
539
657
  errors: ValidationError[];
@@ -548,28 +666,38 @@ interface StageResult {
548
666
  interface ValidationError {
549
667
  code: string;
550
668
  message: string;
669
+ /** Severity of this diagnostic — always `error`. */
670
+ severity?: ValidationSeverity;
551
671
  /** Error position in expression */
552
672
  position?: number;
553
673
  /** Stage the error belongs to */
554
- stage: 'parse' | 'type' | 'semantic' | 'context';
674
+ stage: ValidationStage;
555
675
  /** Whether LLM regeneration is needed */
556
676
  requiresLLM?: boolean;
557
677
  }
558
678
  interface ValidationWarning {
559
679
  code: string;
560
680
  message: string;
681
+ /** Severity of this diagnostic — always `warning`. */
682
+ severity?: ValidationSeverity;
561
683
  /** Warning position in expression */
562
684
  position?: number;
563
685
  /** Stage the warning belongs to */
564
- stage: 'parse' | 'type' | 'semantic' | 'context';
686
+ stage: ValidationStage;
565
687
  }
566
688
  /**
567
- * ValidationPipeline — four-stage validation pipeline.
689
+ * ValidationPipeline — five-stage validation pipeline.
690
+ *
691
+ * 1. Parse: syntax validity (empty, delimiter balance, JS operators, evaluator)
692
+ * 2. Type: type checking (operator-operand type matching) — advisory
693
+ * 3. Semantic: semantic reasonableness — advisory
694
+ * 4. Context: context references (do all references exist in ContextSchema)
695
+ * 5. Final: the expression must parse — structural completeness gate
568
696
  *
569
- * 1. Parse: syntax validity (depends on SpelEvaluator)
570
- * 2. Type: type checking (operator-operand type matching)
571
- * 3. Semantic: semantic reasonableness (is the expression meaningful)
572
- * 4. Context: context references (do all references exist in ContextSchema)
697
+ * Stages 1, 4 and 5 can reject an expression; the type and semantic stages only
698
+ * observe. Reference checks are only meaningful when a schema is supplied,
699
+ * because without one an undeclared `#unknownRef` and a legitimate runtime
700
+ * variable are indistinguishable.
573
701
  */
574
702
  declare class ValidationPipeline {
575
703
  private evaluator;
@@ -581,36 +709,103 @@ declare class ValidationPipeline {
581
709
  validate(expression: string, contextSchema?: ContextSchema): Promise<ValidationResult>;
582
710
  /**
583
711
  * Stage 1: Parse Check — syntax validation
712
+ *
713
+ * Delimiter balance and JavaScript operators are checked against a copy of
714
+ * the expression with string literals blanked out, so literal text can never
715
+ * be mistaken for syntax.
584
716
  */
585
717
  private validateParse;
586
718
  /**
587
- * Stage 2: Type Check — type validation
719
+ * Stage 2: Type Check — advisory type validation
588
720
  */
589
721
  private validateTypes;
590
722
  /**
591
- * Stage 3: Semantic Check — semantic reasonableness validation
723
+ * Stage 3: Semantic Check — advisory semantic validation
592
724
  */
593
725
  private validateSemantic;
594
726
  /**
595
727
  * Stage 4: Context Check — context reference validation
728
+ *
729
+ * A supplied schema turns this stage into a real gate: an undeclared bean,
730
+ * a missing root field or an undeclared variable is an error. Without a
731
+ * schema nothing can be judged, so the stage stays advisory.
596
732
  */
597
733
  private validateContext;
598
734
  /**
599
- * Check if parentheses are balanced
735
+ * Stage 5: Final Check the generated expression must parse.
736
+ *
737
+ * When an evaluator is configured the parse stage already performed a real
738
+ * parse; this mandatory final gate adds the structural check that a truncated
739
+ * tail (an expression ending on an operator) is never accepted, even when no
740
+ * engine is wired in. It is deliberately conservative: only a trailing
741
+ * operator or an unterminated literal fails, so no well-formed expression is
742
+ * rejected.
743
+ */
744
+ private validateFinal;
745
+ /**
746
+ * Whether the expression is obviously incomplete.
747
+ *
748
+ * The last token is obtained from spel-ts's tokenizer, so a trailing
749
+ * operator keyword or an unterminated string literal is detected exactly,
750
+ * and text inside a literal is never read as a trailing operator.
751
+ */
752
+ private isManifestlyIncomplete;
753
+ /**
754
+ * Check if parentheses are balanced. Expects a literal-masked expression.
600
755
  */
601
756
  private hasBalancedParentheses;
602
757
  /**
603
- * Extract all identifier references from expression
758
+ * Extract all identifier references from expression.
759
+ *
760
+ * A bare `#x` that is only the head of a dotted reference (`#x.y`) is not
761
+ * emitted on its own: a dotted reference names a property of some object,
762
+ * not a variable of that name.
604
763
  */
605
764
  private extractReferences;
606
765
  }
607
766
 
608
767
  /**
609
- * AutoFixer — automatically fixes common SpEL syntax errors from LLM output.
768
+ * AutoFixer — rewrites the JavaScript spellings an LLM commonly emits into the
769
+ * SpEL equivalents, without ever touching the contents of a string literal.
770
+ *
771
+ * Literal boundaries come from a quote-state scan. In SpEL a quote character is
772
+ * always a string-literal delimiter — the language has no comments and no other
773
+ * construct that uses `'` or `"` — so the scan is exact by construction rather
774
+ * than an approximation of the parser. It is also independent of the engine
775
+ * build: deriving the boundaries from the lexer made every structural check
776
+ * depend on the lexer being able to tokenize the *whole* expression, which fails
777
+ * for input the engine cannot lex at all, such as a field name in Chinese on a
778
+ * build without Unicode identifier support. A whole-string regex (the previous
779
+ * implementation) rewrote literal contents and appended closers for delimiters
780
+ * it could only see inside literals, turning valid expressions into broken
781
+ * ones.
610
782
  */
611
783
  declare class AutoFixer {
612
784
  fix(expression: string): AutoFixResult;
613
- private fixAllBrackets;
785
+ /**
786
+ * Apply one global replacement rule to every unprotected chunk.
787
+ *
788
+ * The replacement is only reported when it actually matched, and the count
789
+ * is the number of matches across all chunks, so the human-readable change
790
+ * log is unchanged from the previous implementation.
791
+ */
792
+ private applyRule;
793
+ /**
794
+ * Normalise the Elvis operator only when it was written with whitespace
795
+ * between `?` and `:`. A correctly written `?:` must be left untouched so
796
+ * that `fix()` is a no-op on already-valid input.
797
+ */
798
+ private applyElvisRule;
799
+ /**
800
+ * Split the expression into alternating protected (string literal) and
801
+ * unprotected chunks. Ranges come from spel-ts's tokenizer so they agree
802
+ * exactly with what the parser considers opaque.
803
+ */
804
+ private split;
805
+ /**
806
+ * Turn protected spans into a chunk list covering the whole expression.
807
+ */
808
+ private toChunks;
614
809
  }
615
810
  interface AutoFixResult {
616
811
  expression: string;
@@ -676,6 +871,8 @@ interface StrategyResult {
676
871
  interface StrategyMetadata {
677
872
  /** Matched pattern ID (pattern strategy only) */
678
873
  patternId?: string;
874
+ /** Clause texts, when a compound sentence was decomposed */
875
+ clauses?: string[];
679
876
  /** Intent type (template strategy only) */
680
877
  intent?: string;
681
878
  /** Template name (template strategy only) */
@@ -689,6 +886,15 @@ interface StrategyMetadata {
689
886
  /** Raw LLM output (llm strategy only) */
690
887
  rawOutput?: string;
691
888
  }
889
+ /** A compound sentence converted clause by clause. */
890
+ interface DecomposedConversion {
891
+ /** The joined expression, with every operand parenthesised. */
892
+ expression: string;
893
+ /** The clause texts, in order. */
894
+ clauses: string[];
895
+ /** The lowest confidence among the converted clauses. */
896
+ confidence: number;
897
+ }
692
898
  interface StrategyRouterConfig {
693
899
  /** Pattern confidence threshold (default 0.7) */
694
900
  patternMinConfidence?: number;
@@ -718,6 +924,15 @@ declare class StrategyRouter {
718
924
  /**
719
925
  * Get PatternMatcher (for external testing/debugging)
720
926
  */
927
+ /**
928
+ * Convert a sentence that joins its clauses with a logical connector.
929
+ *
930
+ * Returns `null` when `nl` has no top-level connector, in which case the caller
931
+ * should use its ordinary single-pass conversion. Throws
932
+ * {@link UnconvertibleClauseError} when a clause cannot be converted, so the
933
+ * caller can refuse instead of emitting a partial rule.
934
+ */
935
+ decomposeClauses(nl: string): DecomposedConversion | null;
721
936
  getPatternMatcher(): PatternMatcher;
722
937
  /**
723
938
  * Get TemplateEngine
@@ -905,4 +1120,4 @@ declare class ChineseNumberParser {
905
1120
  static parseSafe(text: string): number | null;
906
1121
  }
907
1122
 
908
- export { type AutoFixResult, AutoFixer, BUILTIN_PATTERNS, ChineseNumberParser, ContextExtractor, type CorrectionLog, type DebugInfo, type ExplainResult, type FewShotExample, type GenerateOptions, type GenerateResult, IntentClassifier, type IntentEntity, type IntentResult, type LLMCapabilities, type LLMGenerateOptions, type LLMPrompt, type LLMProvider, type LLMResponse, type LLMStreamChunk, type LLMUsage, NL2SpelEngine, NLIntent, type PatternDefinition, type PatternMatchResult, PatternMatcher, PromptBuilder, type PromptBuilderOptions, ProviderRegistry, SchemaFormatter, type SelfCorrectionConfig, SelfCorrectionLoop, type SelfCorrectionResult, type SlotDefinition, type SlotTransform, type StageResult, type StrategyMetadata, type StrategyResult, StrategyRouter, type StrategyRouterConfig, type StrategyType, TemplateEngine, type TemplateResult, type ValidationError, ValidationPipeline, type ValidationResult, type ValidationWarning };
1123
+ export { type AutoFixResult, AutoFixer, BUILTIN_PATTERNS, ChineseNumberParser, type Clause, ContextExtractor, type CorrectionLog, type DebugInfo, type Decomposition, type ExplainResult, type FewShotExample, type FieldPolicy, type GenerateOptions, type GenerateResult, IntentClassifier, type IntentEntity, type IntentResult, type LLMCapabilities, type LLMGenerateOptions, type LLMPrompt, type LLMProvider, type LLMResponse, type LLMStreamChunk, type LLMUsage, NL2SpelEngine, NLIntent, type PatternDefinition, type PatternMatchResult, PatternMatcher, type PatternMatcherOptions, PromptBuilder, type PromptBuilderOptions, ProviderRegistry, SchemaFormatter, type SelfCorrectionConfig, SelfCorrectionLoop, type SelfCorrectionResult, type SlotDefinition, type SlotTransform, type StageResult, type StrategyMetadata, type StrategyResult, StrategyRouter, type StrategyRouterConfig, type StrategyType, TemplateEngine, type TemplateResult, UnconvertibleClauseError, UnmappedFieldError, type ValidationError, ValidationPipeline, type ValidationResult, type ValidationWarning, decompose, splitClauses };