@sciexpr/ai 0.1.0 → 0.1.1

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.cjs CHANGED
@@ -21,17 +21,34 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AIPipeline: () => AIPipeline,
24
+ AnthropicCompatProvider: () => AnthropicCompatProvider,
24
25
  BUILTIN_PROMPTS: () => BUILTIN_PROMPTS,
25
26
  BuiltinCompletionProvider: () => BuiltinCompletionProvider,
26
27
  BuiltinFormatter: () => BuiltinFormatter,
27
28
  BuiltinKnowledgeBase: () => BuiltinKnowledgeBase,
28
29
  BuiltinNormalizer: () => BuiltinNormalizer,
29
- PromptRegistry: () => PromptRegistry
30
+ OpenAICompatProvider: () => OpenAICompatProvider,
31
+ PromptRegistry: () => PromptRegistry,
32
+ getLastToken: () => getLastToken
30
33
  });
31
34
  module.exports = __toCommonJS(index_exports);
32
35
 
33
36
  // src/fallback.ts
34
37
  var import_core = require("@sciexpr/core");
38
+
39
+ // src/token-utils.ts
40
+ function getLastToken(input) {
41
+ const backslashIdx = input.lastIndexOf("\\");
42
+ if (backslashIdx !== -1) {
43
+ const afterBackslash = input.slice(backslashIdx);
44
+ const match2 = afterBackslash.match(/^\\[a-zA-Z]*$/);
45
+ if (match2) return match2[0];
46
+ }
47
+ const match = input.match(/([a-zA-Z_]+)$/);
48
+ return match ? match[1] : "";
49
+ }
50
+
51
+ // src/fallback.ts
35
52
  var BUILTIN_SYMBOLS = [
36
53
  // Greek lowercase
37
54
  { text: "\\alpha", label: "\u03B1 alpha", category: "symbol", score: 100, description: "Greek alpha" },
@@ -99,16 +116,6 @@ var BuiltinCompletionProvider = class {
99
116
  };
100
117
  }
101
118
  };
102
- function getLastToken(input) {
103
- const backslashIdx = input.lastIndexOf("\\");
104
- if (backslashIdx !== -1) {
105
- const afterBackslash = input.slice(backslashIdx);
106
- const match2 = afterBackslash.match(/^\\[a-zA-Z]*$/);
107
- if (match2) return match2[0];
108
- }
109
- const match = input.match(/([a-zA-Z_]+)$/);
110
- return match ? match[1] : "";
111
- }
112
119
  var CHEMICAL_ELEMENTS = [
113
120
  { symbol: "H", name: "\u6C22 / Hydrogen", atomicNumber: 1, atomicMass: 1.008, group: 1, period: 1, block: "s", electronegativity: 2.2, oxidationStates: [1, -1] },
114
121
  { symbol: "He", name: "\u6C26 / Helium", atomicNumber: 2, atomicMass: 4.0026, group: 18, period: 1, block: "s" },
@@ -265,6 +272,10 @@ var AIPipeline = class {
265
272
  cache;
266
273
  completionCache = /* @__PURE__ */ new Map();
267
274
  kbCache = /* @__PURE__ */ new Map();
275
+ // Cache statistics (L2: actual tracking)
276
+ cacheHits = 0;
277
+ cacheMisses = 0;
278
+ /** AI Pipeline 支持的事件类型 */
268
279
  eventListeners = /* @__PURE__ */ new Map();
269
280
  constructor(config = {}) {
270
281
  this.completionProvider = config.completionProvider ?? new BuiltinCompletionProvider();
@@ -282,7 +293,7 @@ var AIPipeline = class {
282
293
  * 获取智能补全(本地 + AI 混合)
283
294
  */
284
295
  async getCompletions(context) {
285
- const cacheKey = `comp:${context.currentInput}:${context.cursorPosition}`;
296
+ const cacheKey = `comp:${context.currentInput}:${context.cursorPosition}:${context.expressionType}`;
286
297
  const cached = this.getCached(this.completionCache, cacheKey);
287
298
  if (cached) return cached;
288
299
  const localResult = await new BuiltinCompletionProvider().complete(context);
@@ -385,11 +396,28 @@ var AIPipeline = class {
385
396
  isEnabled() {
386
397
  return this.aiEnabled;
387
398
  }
399
+ /** Replace completion provider without losing other configured providers (M1) */
400
+ setCompletionProvider(provider) {
401
+ this.completionProvider = provider;
402
+ this.completionCache.clear();
403
+ }
404
+ /** Replace knowledge base provider without losing other configured providers (M1) */
405
+ setKnowledgeBaseProvider(provider) {
406
+ this.knowledgeBaseProvider = provider;
407
+ this.kbCache.clear();
408
+ }
409
+ /** Replace normalizer provider without losing other configured providers */
410
+ setNormalizerProvider(provider) {
411
+ this.normalizerProvider = provider;
412
+ }
413
+ /** Replace formatter provider without losing other configured providers */
414
+ setFormatterProvider(provider) {
415
+ this.formatterProvider = provider;
416
+ }
388
417
  getCacheStats() {
389
418
  return {
390
- hits: 0,
391
- // Simplified — would need tracking
392
- misses: 0,
419
+ hits: this.cacheHits,
420
+ misses: this.cacheMisses,
393
421
  completionSize: this.completionCache.size,
394
422
  kbSize: this.kbCache.size
395
423
  };
@@ -419,11 +447,18 @@ var AIPipeline = class {
419
447
  getCached(cache, key) {
420
448
  if (!this.cache.enabled) return null;
421
449
  const entry = cache.get(key);
422
- if (!entry) return null;
450
+ if (!entry) {
451
+ this.cacheMisses++;
452
+ return null;
453
+ }
423
454
  if (Date.now() - entry.timestamp > this.cache.ttlMs) {
424
455
  cache.delete(key);
456
+ this.cacheMisses++;
425
457
  return null;
426
458
  }
459
+ this.cacheHits++;
460
+ cache.delete(key);
461
+ cache.set(key, entry);
427
462
  return entry.value;
428
463
  }
429
464
  setCache(cache, key, value) {
@@ -485,6 +520,36 @@ Products: {{products}}
485
520
  Arrow: {{arrow}}
486
521
  Conditions: {{conditions}}`,
487
522
  variables: ["reactants", "products", "arrow", "conditions"]
523
+ },
524
+ /** 物理公式验证 */
525
+ "physics-validation": {
526
+ name: "Physics Validation",
527
+ system: `You are a physics expert. Validate physical formulas.
528
+ Check: dimensional consistency, constant correctness, physical reasonableness.
529
+ Physics notation: \\vec{} for vectors, \\hat{} for operators, \\bra{} / \\ket{} for quantum states.
530
+ Return JSON: {valid: boolean, errors: [{message, code}], warnings: [{message, code}], suggestions: [string]}.`,
531
+ user: `Validate this physics expression:
532
+ Formula: {{formula}}
533
+ Domain: {{domain}}
534
+ Context: {{context}}`,
535
+ variables: ["formula", "domain", "context"]
536
+ },
537
+ /** 自然语言 → 物理公式 */
538
+ "nl-to-physics": {
539
+ name: "Natural Language to Physics Formula",
540
+ system: `Convert natural language physics descriptions to LaTeX expressions.
541
+ Physics conventions:
542
+ - Vector quantities: \\vec{F}, \\vec{v}, \\vec{E}, \\vec{B}
543
+ - Unit vectors: \\hat{i}, \\hat{j}, \\hat{k}, \\hat{r}
544
+ - Operators: \\hat{H}, \\hat{p}, \\hat{x}
545
+ - Quantum: \\bra{}, \\ket{}, \\braket{}
546
+ - Derivatives: \\frac{d}{dt}, \\frac{\\partial}{\\partial x}
547
+ - Integrals: \\int_{a}^{b}, \\oint
548
+ - Greek: \\alpha, \\beta, \\gamma, \\theta, \\phi, \\omega, \\hbar
549
+ - Constants: c (light speed), G (gravitation), h (Planck), \\hbar
550
+ Return JSON: [{text: latex_code, label: display_name, description, category: "formula", score: 1}].`,
551
+ user: `Convert to physics formula: {{query}}`,
552
+ variables: ["query"]
488
553
  }
489
554
  };
490
555
  var PromptRegistry = class {
@@ -533,13 +598,135 @@ var PromptRegistry = class {
533
598
  return this.templates.delete(name);
534
599
  }
535
600
  };
601
+
602
+ // src/providers/openai-compat.ts
603
+ var OpenAICompatProvider = class {
604
+ id = "openai-compat";
605
+ name;
606
+ baseURL;
607
+ apiKey;
608
+ model;
609
+ constructor(config) {
610
+ this.baseURL = config.baseURL.replace(/\/+$/, "");
611
+ this.apiKey = config.apiKey;
612
+ this.model = config.model;
613
+ this.name = `OpenAI Compat (${config.model})`;
614
+ }
615
+ async chat(messages, signal) {
616
+ const url = `${this.baseURL}/chat/completions`;
617
+ const body = {
618
+ model: this.model,
619
+ messages: messages.map((m) => ({
620
+ role: m.role,
621
+ content: m.content
622
+ })),
623
+ temperature: 0.3,
624
+ max_tokens: 2048
625
+ };
626
+ const headers = {
627
+ "Content-Type": "application/json",
628
+ "Authorization": `Bearer ${this.apiKey}`
629
+ };
630
+ const response = await fetch(url, {
631
+ method: "POST",
632
+ headers,
633
+ body: JSON.stringify(body),
634
+ signal
635
+ });
636
+ if (!response.ok) {
637
+ const errorText = await response.text().catch(() => "Unknown error");
638
+ throw new Error(
639
+ `[${this.name}] API error ${response.status}: ${errorText.slice(0, 200)}`
640
+ );
641
+ }
642
+ const data = await response.json();
643
+ const content = data.choices?.[0]?.message?.content;
644
+ if (!content) {
645
+ throw new Error(`[${this.name}] Empty response from API`);
646
+ }
647
+ return content.trim();
648
+ }
649
+ };
650
+
651
+ // src/providers/anthropic-compat.ts
652
+ var AnthropicCompatProvider = class {
653
+ id = "anthropic-compat";
654
+ name;
655
+ baseURL;
656
+ apiKey;
657
+ model;
658
+ apiVersion;
659
+ constructor(config, apiVersion = "2023-06-01") {
660
+ this.baseURL = config.baseURL.replace(/\/+$/, "");
661
+ this.apiKey = config.apiKey;
662
+ this.model = config.model;
663
+ this.apiVersion = apiVersion;
664
+ this.name = `Anthropic Compat (${config.model})`;
665
+ }
666
+ async chat(messages, signal) {
667
+ const url = `${this.baseURL}/v1/messages`;
668
+ const systemMessages = messages.filter((m) => m.role === "system");
669
+ const nonSystemMessages = messages.filter((m) => m.role !== "system");
670
+ const anthropicMessages = nonSystemMessages.map((m) => ({
671
+ role: m.role,
672
+ content: m.content
673
+ }));
674
+ const body = {
675
+ model: this.model,
676
+ messages: anthropicMessages,
677
+ max_tokens: 2048,
678
+ temperature: 0.3
679
+ };
680
+ if (systemMessages.length > 0) {
681
+ body.system = systemMessages.map((m) => m.content).join("\n");
682
+ }
683
+ const headers = {
684
+ "Content-Type": "application/json",
685
+ "x-api-key": this.apiKey,
686
+ "anthropic-version": this.apiVersion
687
+ };
688
+ const response = await fetch(url, {
689
+ method: "POST",
690
+ headers,
691
+ body: JSON.stringify(body),
692
+ signal
693
+ });
694
+ if (!response.ok) {
695
+ const errorText = await response.text().catch(() => "Unknown error");
696
+ throw new Error(
697
+ `[${this.name}] API error ${response.status}: ${errorText.slice(0, 200)}`
698
+ );
699
+ }
700
+ const data = await response.json();
701
+ if (!data.content || data.content.length === 0) {
702
+ throw new Error(`[${this.name}] Empty response from API`);
703
+ }
704
+ const textBlocks = data.content.filter((b) => b.type === "text" && b.text);
705
+ const thinkingBlocks = data.content.filter((b) => b.type === "thinking" && b.thinking);
706
+ let content;
707
+ if (textBlocks.length > 0) {
708
+ content = textBlocks.map((b) => b.text).join("\n").trim();
709
+ } else if (thinkingBlocks.length > 0) {
710
+ content = thinkingBlocks[thinkingBlocks.length - 1].thinking.trim();
711
+ } else {
712
+ throw new Error(`[${this.name}] No text or thinking content in response`);
713
+ }
714
+ if (!content) {
715
+ throw new Error(`[${this.name}] Empty response from API`);
716
+ }
717
+ return content;
718
+ }
719
+ };
536
720
  // Annotate the CommonJS export names for ESM import in node:
537
721
  0 && (module.exports = {
538
722
  AIPipeline,
723
+ AnthropicCompatProvider,
539
724
  BUILTIN_PROMPTS,
540
725
  BuiltinCompletionProvider,
541
726
  BuiltinFormatter,
542
727
  BuiltinKnowledgeBase,
543
728
  BuiltinNormalizer,
544
- PromptRegistry
729
+ OpenAICompatProvider,
730
+ PromptRegistry,
731
+ getLastToken
545
732
  });
package/dist/index.d.cts CHANGED
@@ -322,6 +322,9 @@ declare class AIPipeline {
322
322
  private cache;
323
323
  private completionCache;
324
324
  private kbCache;
325
+ private cacheHits;
326
+ private cacheMisses;
327
+ /** AI Pipeline 支持的事件类型 */
325
328
  private eventListeners;
326
329
  constructor(config?: AIPipelineConfig);
327
330
  /**
@@ -350,6 +353,14 @@ declare class AIPipeline {
350
353
  correct(input: string, error: ValidationError): Promise<CompletionItem[]>;
351
354
  setEnabled(enabled: boolean): void;
352
355
  isEnabled(): boolean;
356
+ /** Replace completion provider without losing other configured providers (M1) */
357
+ setCompletionProvider(provider: IAICompletionProvider): void;
358
+ /** Replace knowledge base provider without losing other configured providers (M1) */
359
+ setKnowledgeBaseProvider(provider: IKnowledgeBaseProvider): void;
360
+ /** Replace normalizer provider without losing other configured providers */
361
+ setNormalizerProvider(provider: IAINormalizerProvider): void;
362
+ /** Replace formatter provider without losing other configured providers */
363
+ setFormatterProvider(provider: IAIFormatterProvider): void;
353
364
  getCacheStats(): {
354
365
  hits: number;
355
366
  misses: number;
@@ -358,7 +369,7 @@ declare class AIPipeline {
358
369
  };
359
370
  clearCache(): void;
360
371
  on(event: string, callback: (...args: any[]) => void): void;
361
- off(event: string, callback: (...args: any[]) => void): void;
372
+ off(event: string, callback: (...args: unknown[]) => void): void;
362
373
  private emit;
363
374
  private getCached;
364
375
  private setCache;
@@ -464,4 +475,109 @@ declare class BuiltinFormatter implements IAIFormatterProvider {
464
475
  format(ast: ExpressionRoot, _style: FormatStyle): Promise<ExpressionRoot>;
465
476
  }
466
477
 
467
- export { type AICacheConfig, AIPipeline, type AIPipelineConfig, BUILTIN_PROMPTS, BuiltinCompletionProvider, BuiltinFormatter, BuiltinKnowledgeBase, BuiltinNormalizer, type CompletionContext, type CompletionItem, type CompletionResult, type ElementInfo, type FormatRule, type FormatStyle, type FormatSuggestion, type FormulaTemplate, type IAICompletionProvider, type IAIFormatterProvider, type IAINormalizerProvider, type IAIStreamAdapter, type IKnowledgeBaseProvider, type KBQueryParams, type KBQueryResult, type NormalizationChange, type NormalizedResult, PromptRegistry, type PromptTemplate, type StreamSource, type SymbolInfo };
478
+ /**
479
+ * 获取最后一个不完整的 LaTeX token
480
+ *
481
+ * 在输入字符串中查找最后一个要进行自动补全的 token。
482
+ * 优先匹配 LaTeX 命令(以 \ 开头),其次匹配普通单词。
483
+ *
484
+ * @param input - 光标之前的输入文本
485
+ * @returns 最后一个不完整 token,如果没有则返回空字符串
486
+ *
487
+ * @example
488
+ * getLastToken('\\fra') // => '\\fra'
489
+ * getLastToken('E = mc') // => 'mc'
490
+ * getLastToken('hello ') // => ''
491
+ */
492
+ declare function getLastToken(input: string): string;
493
+
494
+ /** 聊天消息 */
495
+ interface ChatMessage {
496
+ role: 'system' | 'user' | 'assistant';
497
+ content: string;
498
+ }
499
+ /** AI 提供者配置 */
500
+ interface AIProviderConfig {
501
+ /** API Base URL */
502
+ baseURL: string;
503
+ /** API Key */
504
+ apiKey: string;
505
+ /** 模型名称 */
506
+ model: string;
507
+ }
508
+ /**
509
+ * 聊天补全提供者接口
510
+ *
511
+ * 统一的 LLM 调用接口,OpenAICompatProvider 和 AnthropicCompatProvider 都实现此接口。
512
+ * 返回纯文本,由调用方决定如何插入到编辑器。
513
+ */
514
+ interface IChatCompletionProvider {
515
+ /** 发送消息并返回 AI 生成的文本。signal 用于取消请求 */
516
+ chat(messages: ChatMessage[], signal?: AbortSignal): Promise<string>;
517
+ }
518
+
519
+ /**
520
+ * OpenAI 兼容的聊天补全提供者
521
+ *
522
+ * 支持任何兼容 OpenAI Chat Completions API 的服务:
523
+ * - OpenAI (api.openai.com)
524
+ * - DeepSeek (api.deepseek.com)
525
+ * - 本地 Ollama / vLLM 等服务
526
+ *
527
+ * API: POST {baseURL}/chat/completions
528
+ *
529
+ * @example
530
+ * ```ts
531
+ * const provider = new OpenAICompatProvider({
532
+ * baseURL: 'https://api.deepseek.com/v1',
533
+ * apiKey: 'sk-...',
534
+ * model: 'deepseek-chat',
535
+ * });
536
+ * const result = await provider.chat([
537
+ * { role: 'user', content: '写一个麦克斯韦方程组的 LaTeX 表达式' },
538
+ * ]);
539
+ * ```
540
+ */
541
+ declare class OpenAICompatProvider implements IChatCompletionProvider {
542
+ readonly id = "openai-compat";
543
+ readonly name: string;
544
+ private baseURL;
545
+ private apiKey;
546
+ private model;
547
+ constructor(config: AIProviderConfig);
548
+ chat(messages: ChatMessage[], signal?: AbortSignal): Promise<string>;
549
+ }
550
+
551
+ /**
552
+ * Anthropic 兼容的聊天补全提供者
553
+ *
554
+ * 支持 Anthropic Messages API 的服务:
555
+ * - Anthropic (api.anthropic.com)
556
+ * - 任何兼容 Anthropic API 格式的代理服务
557
+ *
558
+ * API: POST {baseURL}/v1/messages
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * const provider = new AnthropicCompatProvider({
563
+ * baseURL: 'https://api.anthropic.com',
564
+ * apiKey: 'sk-ant-...',
565
+ * model: 'claude-3-5-sonnet-20241022',
566
+ * });
567
+ * const result = await provider.chat([
568
+ * { role: 'user', content: '写一个薛定谔方程的 LaTeX 表达式' },
569
+ * ]);
570
+ * ```
571
+ */
572
+ declare class AnthropicCompatProvider implements IChatCompletionProvider {
573
+ readonly id = "anthropic-compat";
574
+ readonly name: string;
575
+ private baseURL;
576
+ private apiKey;
577
+ private model;
578
+ private apiVersion;
579
+ constructor(config: AIProviderConfig, apiVersion?: string);
580
+ chat(messages: ChatMessage[], signal?: AbortSignal): Promise<string>;
581
+ }
582
+
583
+ export { type AICacheConfig, AIPipeline, type AIPipelineConfig, type AIProviderConfig, AnthropicCompatProvider, BUILTIN_PROMPTS, BuiltinCompletionProvider, BuiltinFormatter, BuiltinKnowledgeBase, BuiltinNormalizer, type ChatMessage, type CompletionContext, type CompletionItem, type CompletionResult, type ElementInfo, type FormatRule, type FormatStyle, type FormatSuggestion, type FormulaTemplate, type IAICompletionProvider, type IAIFormatterProvider, type IAINormalizerProvider, type IAIStreamAdapter, type IChatCompletionProvider, type IKnowledgeBaseProvider, type KBQueryParams, type KBQueryResult, type NormalizationChange, type NormalizedResult, OpenAICompatProvider, PromptRegistry, type PromptTemplate, type StreamSource, type SymbolInfo, getLastToken };
package/dist/index.d.ts CHANGED
@@ -322,6 +322,9 @@ declare class AIPipeline {
322
322
  private cache;
323
323
  private completionCache;
324
324
  private kbCache;
325
+ private cacheHits;
326
+ private cacheMisses;
327
+ /** AI Pipeline 支持的事件类型 */
325
328
  private eventListeners;
326
329
  constructor(config?: AIPipelineConfig);
327
330
  /**
@@ -350,6 +353,14 @@ declare class AIPipeline {
350
353
  correct(input: string, error: ValidationError): Promise<CompletionItem[]>;
351
354
  setEnabled(enabled: boolean): void;
352
355
  isEnabled(): boolean;
356
+ /** Replace completion provider without losing other configured providers (M1) */
357
+ setCompletionProvider(provider: IAICompletionProvider): void;
358
+ /** Replace knowledge base provider without losing other configured providers (M1) */
359
+ setKnowledgeBaseProvider(provider: IKnowledgeBaseProvider): void;
360
+ /** Replace normalizer provider without losing other configured providers */
361
+ setNormalizerProvider(provider: IAINormalizerProvider): void;
362
+ /** Replace formatter provider without losing other configured providers */
363
+ setFormatterProvider(provider: IAIFormatterProvider): void;
353
364
  getCacheStats(): {
354
365
  hits: number;
355
366
  misses: number;
@@ -358,7 +369,7 @@ declare class AIPipeline {
358
369
  };
359
370
  clearCache(): void;
360
371
  on(event: string, callback: (...args: any[]) => void): void;
361
- off(event: string, callback: (...args: any[]) => void): void;
372
+ off(event: string, callback: (...args: unknown[]) => void): void;
362
373
  private emit;
363
374
  private getCached;
364
375
  private setCache;
@@ -464,4 +475,109 @@ declare class BuiltinFormatter implements IAIFormatterProvider {
464
475
  format(ast: ExpressionRoot, _style: FormatStyle): Promise<ExpressionRoot>;
465
476
  }
466
477
 
467
- export { type AICacheConfig, AIPipeline, type AIPipelineConfig, BUILTIN_PROMPTS, BuiltinCompletionProvider, BuiltinFormatter, BuiltinKnowledgeBase, BuiltinNormalizer, type CompletionContext, type CompletionItem, type CompletionResult, type ElementInfo, type FormatRule, type FormatStyle, type FormatSuggestion, type FormulaTemplate, type IAICompletionProvider, type IAIFormatterProvider, type IAINormalizerProvider, type IAIStreamAdapter, type IKnowledgeBaseProvider, type KBQueryParams, type KBQueryResult, type NormalizationChange, type NormalizedResult, PromptRegistry, type PromptTemplate, type StreamSource, type SymbolInfo };
478
+ /**
479
+ * 获取最后一个不完整的 LaTeX token
480
+ *
481
+ * 在输入字符串中查找最后一个要进行自动补全的 token。
482
+ * 优先匹配 LaTeX 命令(以 \ 开头),其次匹配普通单词。
483
+ *
484
+ * @param input - 光标之前的输入文本
485
+ * @returns 最后一个不完整 token,如果没有则返回空字符串
486
+ *
487
+ * @example
488
+ * getLastToken('\\fra') // => '\\fra'
489
+ * getLastToken('E = mc') // => 'mc'
490
+ * getLastToken('hello ') // => ''
491
+ */
492
+ declare function getLastToken(input: string): string;
493
+
494
+ /** 聊天消息 */
495
+ interface ChatMessage {
496
+ role: 'system' | 'user' | 'assistant';
497
+ content: string;
498
+ }
499
+ /** AI 提供者配置 */
500
+ interface AIProviderConfig {
501
+ /** API Base URL */
502
+ baseURL: string;
503
+ /** API Key */
504
+ apiKey: string;
505
+ /** 模型名称 */
506
+ model: string;
507
+ }
508
+ /**
509
+ * 聊天补全提供者接口
510
+ *
511
+ * 统一的 LLM 调用接口,OpenAICompatProvider 和 AnthropicCompatProvider 都实现此接口。
512
+ * 返回纯文本,由调用方决定如何插入到编辑器。
513
+ */
514
+ interface IChatCompletionProvider {
515
+ /** 发送消息并返回 AI 生成的文本。signal 用于取消请求 */
516
+ chat(messages: ChatMessage[], signal?: AbortSignal): Promise<string>;
517
+ }
518
+
519
+ /**
520
+ * OpenAI 兼容的聊天补全提供者
521
+ *
522
+ * 支持任何兼容 OpenAI Chat Completions API 的服务:
523
+ * - OpenAI (api.openai.com)
524
+ * - DeepSeek (api.deepseek.com)
525
+ * - 本地 Ollama / vLLM 等服务
526
+ *
527
+ * API: POST {baseURL}/chat/completions
528
+ *
529
+ * @example
530
+ * ```ts
531
+ * const provider = new OpenAICompatProvider({
532
+ * baseURL: 'https://api.deepseek.com/v1',
533
+ * apiKey: 'sk-...',
534
+ * model: 'deepseek-chat',
535
+ * });
536
+ * const result = await provider.chat([
537
+ * { role: 'user', content: '写一个麦克斯韦方程组的 LaTeX 表达式' },
538
+ * ]);
539
+ * ```
540
+ */
541
+ declare class OpenAICompatProvider implements IChatCompletionProvider {
542
+ readonly id = "openai-compat";
543
+ readonly name: string;
544
+ private baseURL;
545
+ private apiKey;
546
+ private model;
547
+ constructor(config: AIProviderConfig);
548
+ chat(messages: ChatMessage[], signal?: AbortSignal): Promise<string>;
549
+ }
550
+
551
+ /**
552
+ * Anthropic 兼容的聊天补全提供者
553
+ *
554
+ * 支持 Anthropic Messages API 的服务:
555
+ * - Anthropic (api.anthropic.com)
556
+ * - 任何兼容 Anthropic API 格式的代理服务
557
+ *
558
+ * API: POST {baseURL}/v1/messages
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * const provider = new AnthropicCompatProvider({
563
+ * baseURL: 'https://api.anthropic.com',
564
+ * apiKey: 'sk-ant-...',
565
+ * model: 'claude-3-5-sonnet-20241022',
566
+ * });
567
+ * const result = await provider.chat([
568
+ * { role: 'user', content: '写一个薛定谔方程的 LaTeX 表达式' },
569
+ * ]);
570
+ * ```
571
+ */
572
+ declare class AnthropicCompatProvider implements IChatCompletionProvider {
573
+ readonly id = "anthropic-compat";
574
+ readonly name: string;
575
+ private baseURL;
576
+ private apiKey;
577
+ private model;
578
+ private apiVersion;
579
+ constructor(config: AIProviderConfig, apiVersion?: string);
580
+ chat(messages: ChatMessage[], signal?: AbortSignal): Promise<string>;
581
+ }
582
+
583
+ export { type AICacheConfig, AIPipeline, type AIPipelineConfig, type AIProviderConfig, AnthropicCompatProvider, BUILTIN_PROMPTS, BuiltinCompletionProvider, BuiltinFormatter, BuiltinKnowledgeBase, BuiltinNormalizer, type ChatMessage, type CompletionContext, type CompletionItem, type CompletionResult, type ElementInfo, type FormatRule, type FormatStyle, type FormatSuggestion, type FormulaTemplate, type IAICompletionProvider, type IAIFormatterProvider, type IAINormalizerProvider, type IAIStreamAdapter, type IChatCompletionProvider, type IKnowledgeBaseProvider, type KBQueryParams, type KBQueryResult, type NormalizationChange, type NormalizedResult, OpenAICompatProvider, PromptRegistry, type PromptTemplate, type StreamSource, type SymbolInfo, getLastToken };
package/dist/index.js CHANGED
@@ -1,5 +1,19 @@
1
1
  // src/fallback.ts
2
2
  import { ExpressionKind, createRoot, createText } from "@sciexpr/core";
3
+
4
+ // src/token-utils.ts
5
+ function getLastToken(input) {
6
+ const backslashIdx = input.lastIndexOf("\\");
7
+ if (backslashIdx !== -1) {
8
+ const afterBackslash = input.slice(backslashIdx);
9
+ const match2 = afterBackslash.match(/^\\[a-zA-Z]*$/);
10
+ if (match2) return match2[0];
11
+ }
12
+ const match = input.match(/([a-zA-Z_]+)$/);
13
+ return match ? match[1] : "";
14
+ }
15
+
16
+ // src/fallback.ts
3
17
  var BUILTIN_SYMBOLS = [
4
18
  // Greek lowercase
5
19
  { text: "\\alpha", label: "\u03B1 alpha", category: "symbol", score: 100, description: "Greek alpha" },
@@ -67,16 +81,6 @@ var BuiltinCompletionProvider = class {
67
81
  };
68
82
  }
69
83
  };
70
- function getLastToken(input) {
71
- const backslashIdx = input.lastIndexOf("\\");
72
- if (backslashIdx !== -1) {
73
- const afterBackslash = input.slice(backslashIdx);
74
- const match2 = afterBackslash.match(/^\\[a-zA-Z]*$/);
75
- if (match2) return match2[0];
76
- }
77
- const match = input.match(/([a-zA-Z_]+)$/);
78
- return match ? match[1] : "";
79
- }
80
84
  var CHEMICAL_ELEMENTS = [
81
85
  { symbol: "H", name: "\u6C22 / Hydrogen", atomicNumber: 1, atomicMass: 1.008, group: 1, period: 1, block: "s", electronegativity: 2.2, oxidationStates: [1, -1] },
82
86
  { symbol: "He", name: "\u6C26 / Helium", atomicNumber: 2, atomicMass: 4.0026, group: 18, period: 1, block: "s" },
@@ -233,6 +237,10 @@ var AIPipeline = class {
233
237
  cache;
234
238
  completionCache = /* @__PURE__ */ new Map();
235
239
  kbCache = /* @__PURE__ */ new Map();
240
+ // Cache statistics (L2: actual tracking)
241
+ cacheHits = 0;
242
+ cacheMisses = 0;
243
+ /** AI Pipeline 支持的事件类型 */
236
244
  eventListeners = /* @__PURE__ */ new Map();
237
245
  constructor(config = {}) {
238
246
  this.completionProvider = config.completionProvider ?? new BuiltinCompletionProvider();
@@ -250,7 +258,7 @@ var AIPipeline = class {
250
258
  * 获取智能补全(本地 + AI 混合)
251
259
  */
252
260
  async getCompletions(context) {
253
- const cacheKey = `comp:${context.currentInput}:${context.cursorPosition}`;
261
+ const cacheKey = `comp:${context.currentInput}:${context.cursorPosition}:${context.expressionType}`;
254
262
  const cached = this.getCached(this.completionCache, cacheKey);
255
263
  if (cached) return cached;
256
264
  const localResult = await new BuiltinCompletionProvider().complete(context);
@@ -353,11 +361,28 @@ var AIPipeline = class {
353
361
  isEnabled() {
354
362
  return this.aiEnabled;
355
363
  }
364
+ /** Replace completion provider without losing other configured providers (M1) */
365
+ setCompletionProvider(provider) {
366
+ this.completionProvider = provider;
367
+ this.completionCache.clear();
368
+ }
369
+ /** Replace knowledge base provider without losing other configured providers (M1) */
370
+ setKnowledgeBaseProvider(provider) {
371
+ this.knowledgeBaseProvider = provider;
372
+ this.kbCache.clear();
373
+ }
374
+ /** Replace normalizer provider without losing other configured providers */
375
+ setNormalizerProvider(provider) {
376
+ this.normalizerProvider = provider;
377
+ }
378
+ /** Replace formatter provider without losing other configured providers */
379
+ setFormatterProvider(provider) {
380
+ this.formatterProvider = provider;
381
+ }
356
382
  getCacheStats() {
357
383
  return {
358
- hits: 0,
359
- // Simplified — would need tracking
360
- misses: 0,
384
+ hits: this.cacheHits,
385
+ misses: this.cacheMisses,
361
386
  completionSize: this.completionCache.size,
362
387
  kbSize: this.kbCache.size
363
388
  };
@@ -387,11 +412,18 @@ var AIPipeline = class {
387
412
  getCached(cache, key) {
388
413
  if (!this.cache.enabled) return null;
389
414
  const entry = cache.get(key);
390
- if (!entry) return null;
415
+ if (!entry) {
416
+ this.cacheMisses++;
417
+ return null;
418
+ }
391
419
  if (Date.now() - entry.timestamp > this.cache.ttlMs) {
392
420
  cache.delete(key);
421
+ this.cacheMisses++;
393
422
  return null;
394
423
  }
424
+ this.cacheHits++;
425
+ cache.delete(key);
426
+ cache.set(key, entry);
395
427
  return entry.value;
396
428
  }
397
429
  setCache(cache, key, value) {
@@ -453,6 +485,36 @@ Products: {{products}}
453
485
  Arrow: {{arrow}}
454
486
  Conditions: {{conditions}}`,
455
487
  variables: ["reactants", "products", "arrow", "conditions"]
488
+ },
489
+ /** 物理公式验证 */
490
+ "physics-validation": {
491
+ name: "Physics Validation",
492
+ system: `You are a physics expert. Validate physical formulas.
493
+ Check: dimensional consistency, constant correctness, physical reasonableness.
494
+ Physics notation: \\vec{} for vectors, \\hat{} for operators, \\bra{} / \\ket{} for quantum states.
495
+ Return JSON: {valid: boolean, errors: [{message, code}], warnings: [{message, code}], suggestions: [string]}.`,
496
+ user: `Validate this physics expression:
497
+ Formula: {{formula}}
498
+ Domain: {{domain}}
499
+ Context: {{context}}`,
500
+ variables: ["formula", "domain", "context"]
501
+ },
502
+ /** 自然语言 → 物理公式 */
503
+ "nl-to-physics": {
504
+ name: "Natural Language to Physics Formula",
505
+ system: `Convert natural language physics descriptions to LaTeX expressions.
506
+ Physics conventions:
507
+ - Vector quantities: \\vec{F}, \\vec{v}, \\vec{E}, \\vec{B}
508
+ - Unit vectors: \\hat{i}, \\hat{j}, \\hat{k}, \\hat{r}
509
+ - Operators: \\hat{H}, \\hat{p}, \\hat{x}
510
+ - Quantum: \\bra{}, \\ket{}, \\braket{}
511
+ - Derivatives: \\frac{d}{dt}, \\frac{\\partial}{\\partial x}
512
+ - Integrals: \\int_{a}^{b}, \\oint
513
+ - Greek: \\alpha, \\beta, \\gamma, \\theta, \\phi, \\omega, \\hbar
514
+ - Constants: c (light speed), G (gravitation), h (Planck), \\hbar
515
+ Return JSON: [{text: latex_code, label: display_name, description, category: "formula", score: 1}].`,
516
+ user: `Convert to physics formula: {{query}}`,
517
+ variables: ["query"]
456
518
  }
457
519
  };
458
520
  var PromptRegistry = class {
@@ -501,12 +563,134 @@ var PromptRegistry = class {
501
563
  return this.templates.delete(name);
502
564
  }
503
565
  };
566
+
567
+ // src/providers/openai-compat.ts
568
+ var OpenAICompatProvider = class {
569
+ id = "openai-compat";
570
+ name;
571
+ baseURL;
572
+ apiKey;
573
+ model;
574
+ constructor(config) {
575
+ this.baseURL = config.baseURL.replace(/\/+$/, "");
576
+ this.apiKey = config.apiKey;
577
+ this.model = config.model;
578
+ this.name = `OpenAI Compat (${config.model})`;
579
+ }
580
+ async chat(messages, signal) {
581
+ const url = `${this.baseURL}/chat/completions`;
582
+ const body = {
583
+ model: this.model,
584
+ messages: messages.map((m) => ({
585
+ role: m.role,
586
+ content: m.content
587
+ })),
588
+ temperature: 0.3,
589
+ max_tokens: 2048
590
+ };
591
+ const headers = {
592
+ "Content-Type": "application/json",
593
+ "Authorization": `Bearer ${this.apiKey}`
594
+ };
595
+ const response = await fetch(url, {
596
+ method: "POST",
597
+ headers,
598
+ body: JSON.stringify(body),
599
+ signal
600
+ });
601
+ if (!response.ok) {
602
+ const errorText = await response.text().catch(() => "Unknown error");
603
+ throw new Error(
604
+ `[${this.name}] API error ${response.status}: ${errorText.slice(0, 200)}`
605
+ );
606
+ }
607
+ const data = await response.json();
608
+ const content = data.choices?.[0]?.message?.content;
609
+ if (!content) {
610
+ throw new Error(`[${this.name}] Empty response from API`);
611
+ }
612
+ return content.trim();
613
+ }
614
+ };
615
+
616
+ // src/providers/anthropic-compat.ts
617
+ var AnthropicCompatProvider = class {
618
+ id = "anthropic-compat";
619
+ name;
620
+ baseURL;
621
+ apiKey;
622
+ model;
623
+ apiVersion;
624
+ constructor(config, apiVersion = "2023-06-01") {
625
+ this.baseURL = config.baseURL.replace(/\/+$/, "");
626
+ this.apiKey = config.apiKey;
627
+ this.model = config.model;
628
+ this.apiVersion = apiVersion;
629
+ this.name = `Anthropic Compat (${config.model})`;
630
+ }
631
+ async chat(messages, signal) {
632
+ const url = `${this.baseURL}/v1/messages`;
633
+ const systemMessages = messages.filter((m) => m.role === "system");
634
+ const nonSystemMessages = messages.filter((m) => m.role !== "system");
635
+ const anthropicMessages = nonSystemMessages.map((m) => ({
636
+ role: m.role,
637
+ content: m.content
638
+ }));
639
+ const body = {
640
+ model: this.model,
641
+ messages: anthropicMessages,
642
+ max_tokens: 2048,
643
+ temperature: 0.3
644
+ };
645
+ if (systemMessages.length > 0) {
646
+ body.system = systemMessages.map((m) => m.content).join("\n");
647
+ }
648
+ const headers = {
649
+ "Content-Type": "application/json",
650
+ "x-api-key": this.apiKey,
651
+ "anthropic-version": this.apiVersion
652
+ };
653
+ const response = await fetch(url, {
654
+ method: "POST",
655
+ headers,
656
+ body: JSON.stringify(body),
657
+ signal
658
+ });
659
+ if (!response.ok) {
660
+ const errorText = await response.text().catch(() => "Unknown error");
661
+ throw new Error(
662
+ `[${this.name}] API error ${response.status}: ${errorText.slice(0, 200)}`
663
+ );
664
+ }
665
+ const data = await response.json();
666
+ if (!data.content || data.content.length === 0) {
667
+ throw new Error(`[${this.name}] Empty response from API`);
668
+ }
669
+ const textBlocks = data.content.filter((b) => b.type === "text" && b.text);
670
+ const thinkingBlocks = data.content.filter((b) => b.type === "thinking" && b.thinking);
671
+ let content;
672
+ if (textBlocks.length > 0) {
673
+ content = textBlocks.map((b) => b.text).join("\n").trim();
674
+ } else if (thinkingBlocks.length > 0) {
675
+ content = thinkingBlocks[thinkingBlocks.length - 1].thinking.trim();
676
+ } else {
677
+ throw new Error(`[${this.name}] No text or thinking content in response`);
678
+ }
679
+ if (!content) {
680
+ throw new Error(`[${this.name}] Empty response from API`);
681
+ }
682
+ return content;
683
+ }
684
+ };
504
685
  export {
505
686
  AIPipeline,
687
+ AnthropicCompatProvider,
506
688
  BUILTIN_PROMPTS,
507
689
  BuiltinCompletionProvider,
508
690
  BuiltinFormatter,
509
691
  BuiltinKnowledgeBase,
510
692
  BuiltinNormalizer,
511
- PromptRegistry
693
+ OpenAICompatProvider,
694
+ PromptRegistry,
695
+ getLastToken
512
696
  };
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@sciexpr/ai",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Scientific Expression Engine - AI Provider SPI interfaces & built-in fallbacks",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
- "module": "./dist/index.mjs",
7
+ "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
- "import": "./dist/index.mjs",
12
+ "import": "./dist/index.js",
13
13
  "require": "./dist/index.cjs"
14
14
  }
15
15
  },
@@ -43,7 +43,7 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@sciexpr/core": "0.1.0"
46
+ "@sciexpr/core": "0.1.1"
47
47
  },
48
48
  "devDependencies": {
49
49
  "tsup": "^8.0.0",