@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 +204 -17
- package/dist/index.d.cts +118 -2
- package/dist/index.d.ts +118 -2
- package/dist/index.js +200 -16
- package/package.json +4 -4
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
|
-
|
|
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:
|
|
391
|
-
|
|
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)
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
359
|
-
|
|
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)
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
46
|
+
"@sciexpr/core": "0.1.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"tsup": "^8.0.0",
|