@futdevpro/nts-dynamo 1.15.23 → 1.15.29
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/README.md +4 -0
- package/__documentations/2026-05-17-oai-compatible-providers.md +229 -0
- package/_specifications/BACKLOG.md +28 -0
- package/build/_models/interfaces/compare-data-options.interface.d.ts +27 -0
- package/build/_models/interfaces/compare-data-options.interface.d.ts.map +1 -0
- package/build/_models/interfaces/compare-data-options.interface.js +3 -0
- package/build/_models/interfaces/compare-data-options.interface.js.map +1 -0
- package/build/_models/interfaces/compare-data-result.interface.d.ts +13 -0
- package/build/_models/interfaces/compare-data-result.interface.d.ts.map +1 -0
- package/build/_models/interfaces/compare-data-result.interface.js +3 -0
- package/build/_models/interfaces/compare-data-result.interface.js.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.d.ts +14 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.d.ts.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.js +3 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.js.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.d.ts +50 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.d.ts.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.js +3 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.js.map +1 -0
- package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.d.ts.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.js +32 -0
- package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.js.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.d.ts.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.js +20 -2
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.js.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts +4 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js +28 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js.map +1 -1
- package/build/_modules/ai/_services/ai-provider.service-base.d.ts +21 -0
- package/build/_modules/ai/_services/ai-provider.service-base.d.ts.map +1 -1
- package/build/_modules/ai/_services/ai-provider.service-base.js +32 -0
- package/build/_modules/ai/_services/ai-provider.service-base.js.map +1 -1
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.d.ts +17 -1
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.d.ts.map +1 -1
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.js +16 -0
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.js.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.d.ts +89 -0
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.d.ts.map +1 -0
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.js +190 -0
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.js.map +1 -0
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.d.ts +18 -2
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.d.ts.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.js +57 -3
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.js.map +1 -1
- package/build/_services/base/data.service.d.ts +63 -0
- package/build/_services/base/data.service.d.ts.map +1 -1
- package/build/_services/base/data.service.js +189 -0
- package/build/_services/base/data.service.js.map +1 -1
- package/package.json +1 -1
- package/src/_models/interfaces/compare-data-options.interface.ts +27 -0
- package/src/_models/interfaces/compare-data-result.interface.ts +12 -0
- package/src/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.ts +14 -0
- package/src/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.ts +56 -0
- package/src/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.spec.ts +92 -0
- package/src/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.ts +38 -4
- package/src/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.ts +24 -5
- package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.spec.ts +52 -0
- package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.ts +39 -10
- package/src/_modules/ai/_services/ai-provider.service-base.spec.ts +79 -0
- package/src/_modules/ai/_services/ai-provider.service-base.ts +41 -3
- package/src/_modules/local-vector-search/_enums/lvs-search-mode.enum.ts +16 -0
- package/src/_modules/local-vector-search/_services/lvs-bm25.util.spec.ts +159 -0
- package/src/_modules/local-vector-search/_services/lvs-bm25.util.ts +206 -0
- package/src/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.spec.ts +135 -0
- package/src/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.ts +95 -9
- package/src/_services/base/data.service.spec.ts +181 -0
- package/src/_services/base/data.service.ts +196 -2
|
@@ -343,6 +343,58 @@ describe('| DyNTS_OAI_LLM_ServiceBase', () => {
|
|
|
343
343
|
});
|
|
344
344
|
});
|
|
345
345
|
|
|
346
|
+
describe('| FR-002 cost-event emit', () => {
|
|
347
|
+
|
|
348
|
+
it('| should emit cost-event with llm-completion callType for single user message', async () => {
|
|
349
|
+
const received: any[] = [];
|
|
350
|
+
(service as any).onCostEvent = (e: any) => { received.push(e); };
|
|
351
|
+
|
|
352
|
+
await service.resolveMessage({
|
|
353
|
+
conversation: [{ role: DyFM_AI_MessageRole.user, content: 'Q?' }],
|
|
354
|
+
issuer: 'llm-issuer-1',
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(received.length).toBe(1);
|
|
358
|
+
expect(received[0].callType).toBe('llm-completion');
|
|
359
|
+
expect(received[0].provider).toBe('open-ai');
|
|
360
|
+
expect(received[0].model).toBe(DyFM_OAI_Model.gpt4o_mini);
|
|
361
|
+
expect(received[0].tokensUsed.input).toBe(10);
|
|
362
|
+
expect(received[0].tokensUsed.output).toBe(5);
|
|
363
|
+
expect(received[0].tokensUsed.total).toBe(15);
|
|
364
|
+
expect(received[0].issuer).toBe('llm-issuer-1');
|
|
365
|
+
expect(received[0].timestamp).toBeInstanceOf(Date);
|
|
366
|
+
expect(received[0].durationMs).toBeGreaterThanOrEqual(0);
|
|
367
|
+
expect(received[0].estimatedCostUsd).toBeUndefined();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('| should emit cost-event with llm-chat callType for multi-message conversation', async () => {
|
|
371
|
+
const received: any[] = [];
|
|
372
|
+
(service as any).onCostEvent = (e: any) => { received.push(e); };
|
|
373
|
+
|
|
374
|
+
await service.resolveMessage({
|
|
375
|
+
conversation: [
|
|
376
|
+
{ role: DyFM_AI_MessageRole.user, content: 'Q1?' },
|
|
377
|
+
{ role: DyFM_AI_MessageRole.assistant, content: 'A1.' },
|
|
378
|
+
{ role: DyFM_AI_MessageRole.user, content: 'Q2?' },
|
|
379
|
+
],
|
|
380
|
+
issuer: 'llm-issuer-chat',
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(received.length).toBe(1);
|
|
384
|
+
expect(received[0].callType).toBe('llm-chat');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('| constructor accepts onCostEvent option (non-breaking additive)', () => {
|
|
388
|
+
const cb: any = jasmine.createSpy('onCostEvent');
|
|
389
|
+
const s: DyNTS_OAI_LLM_ServiceBase = new DyNTS_OAI_LLM_ServiceBase({
|
|
390
|
+
...mockSettings,
|
|
391
|
+
onCostEvent: cb,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect((s as any).onCostEvent).toBe(cb);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
346
398
|
describe('| getMessageCreateInput', () => {
|
|
347
399
|
it('| should create input with default settings', () => {
|
|
348
400
|
const conversation: DyFM_AI_Message[] = [
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { DyNTS_global_settings } from '../../../../../_collections/global-settings.const';
|
|
16
16
|
import { ChatCompletion } from 'openai/resources';
|
|
17
17
|
import { ChatCompletionCreateParamsBase, ChatCompletionMessageParam } from 'openai/resources/chat/completions';
|
|
18
|
+
import { DyNTS_AI_CostEventCallback } from '../../../_models/interfaces/dynts-ai-cost-event-callback.interface';
|
|
18
19
|
import { DyNTS_OAI_LLM_Predefined_Requests } from '../_models/interfaces/oai-llm-predefined-requests.interface';
|
|
19
20
|
import { DyNTS_OAI_global_settings } from '../_collections/oai-global-settings.const';
|
|
20
21
|
import { DyNTS_OAI_LLMDefaultPredefined_Requests } from '../_collections/oai-llm-predefined-requests.conts';
|
|
@@ -82,14 +83,14 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
|
|
|
82
83
|
predefinedRequests: DyNTS_OAI_LLM_Predefined_Requests = DyNTS_OAI_LLMDefaultPredefined_Requests;
|
|
83
84
|
|
|
84
85
|
constructor(
|
|
85
|
-
set?: DyFM_OAI_Settings
|
|
86
|
+
set?: DyFM_OAI_Settings & { onCostEvent?: DyNTS_AI_CostEventCallback }
|
|
86
87
|
) {
|
|
87
88
|
super();
|
|
88
|
-
|
|
89
|
+
|
|
89
90
|
DyNTS_global_settings.openAi_settings ??= DyNTS_OAI_global_settings;
|
|
90
|
-
|
|
91
|
+
|
|
91
92
|
this.openai = new OpenAI(
|
|
92
|
-
set?.config ??
|
|
93
|
+
set?.config ??
|
|
93
94
|
{
|
|
94
95
|
organization: DyNTS_global_settings.env_settings.openAi.organization,
|
|
95
96
|
apiKey: DyNTS_global_settings.env_settings.openAi.apiKey,
|
|
@@ -97,9 +98,14 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
|
|
|
97
98
|
}
|
|
98
99
|
);
|
|
99
100
|
|
|
100
|
-
this.defaultSettings = set?.defaultSettings ??
|
|
101
|
-
DyNTS_OAI_global_settings.defaultSettings ??
|
|
101
|
+
this.defaultSettings = set?.defaultSettings ??
|
|
102
|
+
DyNTS_OAI_global_settings.defaultSettings ??
|
|
102
103
|
new DyFM_OAI_CallSettings();
|
|
104
|
+
|
|
105
|
+
// FR-002 — opcionális per-call cost-event callback átadás a base-osztálynak
|
|
106
|
+
if (set?.onCostEvent) {
|
|
107
|
+
this.onCostEvent = set.onCostEvent;
|
|
108
|
+
}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/**
|
|
@@ -445,13 +451,36 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
|
|
|
445
451
|
} */
|
|
446
452
|
): Promise<string | ChatCompletion> {
|
|
447
453
|
try {
|
|
448
|
-
|
|
449
|
-
|
|
454
|
+
// FR-002 — callType meghatározás a unshift ELŐTT (a system-message hozzáadás
|
|
455
|
+
// után már 2+ message lenne, ami a "chat" kategóriát mindig kiváltja).
|
|
456
|
+
// 1 user-message → 'llm-completion'; több → 'llm-chat'. A 'llm-tool-use'
|
|
457
|
+
// categoria a tools paraméterek bevezetésekor kerül használatba (jövőbeli FR).
|
|
458
|
+
const userMessageCount: number = set.conversation.length;
|
|
450
459
|
|
|
460
|
+
set.conversation.unshift(this.getDefaultSystemMessage(set.settings));
|
|
451
461
|
|
|
462
|
+
// FR-002 — per-call cost-event timing: a tényleges API-call latency mérve
|
|
463
|
+
const input: ChatCompletionCreateParamsBase = this.getMessageCreateInput(set);
|
|
464
|
+
const start: number = Date.now();
|
|
452
465
|
const result: ChatCompletion = await this.openai.chat.completions.create(
|
|
453
|
-
|
|
466
|
+
input,
|
|
454
467
|
) as ChatCompletion;
|
|
468
|
+
const durationMs: number = Date.now() - start;
|
|
469
|
+
|
|
470
|
+
// FR-002 — cost-event emit (safe, no-op ha nincs onCostEvent callback)
|
|
471
|
+
this.emitCostEvent({
|
|
472
|
+
callType: userMessageCount === 1 ? 'llm-completion' : 'llm-chat',
|
|
473
|
+
provider: this.aiProvider,
|
|
474
|
+
model: String(input.model ?? this.defaultModel),
|
|
475
|
+
tokensUsed: {
|
|
476
|
+
input: result.usage?.prompt_tokens ?? 0,
|
|
477
|
+
output: result.usage?.completion_tokens ?? 0,
|
|
478
|
+
total: result.usage?.total_tokens ?? 0,
|
|
479
|
+
},
|
|
480
|
+
durationMs: durationMs,
|
|
481
|
+
issuer: set.issuer,
|
|
482
|
+
timestamp: new Date(),
|
|
483
|
+
});
|
|
455
484
|
|
|
456
485
|
if (set.getFullResponse) {
|
|
457
486
|
return result;
|
|
@@ -461,7 +490,7 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
|
|
|
461
490
|
} catch (error) {
|
|
462
491
|
throw new DyFM_Error({
|
|
463
492
|
...this.getDefaultErrorSettings('resolveConversation', error, set.issuer),
|
|
464
|
-
|
|
493
|
+
|
|
465
494
|
errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OLSB-RM0`,
|
|
466
495
|
});
|
|
467
496
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
|
|
2
|
+
import { DyNTS_AI_CostEvent } from '../_models/interfaces/dynts-ai-cost-event.interface';
|
|
3
|
+
import { DyNTS_AI_CostEventCallback } from '../_models/interfaces/dynts-ai-cost-event-callback.interface';
|
|
2
4
|
import { DyNTS_AI_Provider_ServiceBase } from './ai-provider.service-base';
|
|
3
5
|
import { DyFM_AI_Provider, DyFM_AI_ProviderCapabilities, DyFM_AI_Config } from '@futdevpro/fsm-dynamo/ai';
|
|
4
6
|
|
|
@@ -25,11 +27,35 @@ class TestProviderService extends DyNTS_AI_Provider_ServiceBase {
|
|
|
25
27
|
return true;
|
|
26
28
|
};
|
|
27
29
|
|
|
30
|
+
/** Test-only helper: ráad egy callback-et a protected onCostEvent fielden át. */
|
|
31
|
+
public setOnCostEventForTest(cb: DyNTS_AI_CostEventCallback | undefined): void {
|
|
32
|
+
this.onCostEvent = cb;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Test-only helper: meghívja a protected emitCostEvent-et (a sub-class amúgy is hívná). */
|
|
36
|
+
public emitForTest(event: DyNTS_AI_CostEvent): void {
|
|
37
|
+
this.emitCostEvent(event);
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
static getInstance(): TestProviderService {
|
|
29
41
|
return TestProviderService.getSingletonInstance();
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
44
|
|
|
45
|
+
/** Helper: minimal valid DyNTS_AI_CostEvent. */
|
|
46
|
+
function makeCostEvent(overrides: Partial<DyNTS_AI_CostEvent> = {}): DyNTS_AI_CostEvent {
|
|
47
|
+
return {
|
|
48
|
+
callType: 'embedding-single',
|
|
49
|
+
provider: 'openai',
|
|
50
|
+
model: 'text-embedding-3-small',
|
|
51
|
+
tokensUsed: { input: 10, total: 10 },
|
|
52
|
+
durationMs: 42,
|
|
53
|
+
issuer: 'test-issuer',
|
|
54
|
+
timestamp: new Date(),
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
describe('| DyNTS_AI_Provider_ServiceBase', () => {
|
|
34
60
|
let service: TestProviderService;
|
|
35
61
|
|
|
@@ -75,5 +101,58 @@ describe('| DyNTS_AI_Provider_ServiceBase', () => {
|
|
|
75
101
|
expect(result).toBe(true);
|
|
76
102
|
});
|
|
77
103
|
});
|
|
104
|
+
|
|
105
|
+
describe('| emitCostEvent (FR-002 cost-event hook)', () => {
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
// Reset callback hogy ne szivárogjon át teszt-cases között (singleton service)
|
|
109
|
+
service.setOnCostEventForTest(undefined);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('| should be no-op when onCostEvent is undefined', () => {
|
|
113
|
+
service.setOnCostEventForTest(undefined);
|
|
114
|
+
|
|
115
|
+
expect(() => {
|
|
116
|
+
service.emitForTest(makeCostEvent());
|
|
117
|
+
}).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('| should invoke the registered callback with the event payload', () => {
|
|
121
|
+
const received: DyNTS_AI_CostEvent[] = [];
|
|
122
|
+
service.setOnCostEventForTest((e: DyNTS_AI_CostEvent) => {
|
|
123
|
+
received.push(e);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const event: DyNTS_AI_CostEvent = makeCostEvent({
|
|
127
|
+
model: 'gpt-4o-mini',
|
|
128
|
+
callType: 'llm-completion',
|
|
129
|
+
tokensUsed: { input: 100, output: 50, total: 150 },
|
|
130
|
+
});
|
|
131
|
+
service.emitForTest(event);
|
|
132
|
+
|
|
133
|
+
expect(received.length).toBe(1);
|
|
134
|
+
expect(received[0]).toEqual(event);
|
|
135
|
+
expect(received[0].tokensUsed.total).toBe(150);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('| should NOT throw when the callback itself throws (safe emit)', () => {
|
|
139
|
+
service.setOnCostEventForTest(() => {
|
|
140
|
+
throw new Error('consumer callback failure');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(() => {
|
|
144
|
+
service.emitForTest(makeCostEvent());
|
|
145
|
+
}).not.toThrow();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('| should leave estimatedCostUsd undefined by default (Dynamo has no pricing registry)', () => {
|
|
149
|
+
const received: DyNTS_AI_CostEvent[] = [];
|
|
150
|
+
service.setOnCostEventForTest((e: DyNTS_AI_CostEvent) => { received.push(e); });
|
|
151
|
+
|
|
152
|
+
service.emitForTest(makeCostEvent());
|
|
153
|
+
|
|
154
|
+
expect(received[0].estimatedCostUsd).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
78
157
|
});
|
|
79
158
|
|
|
@@ -1,29 +1,67 @@
|
|
|
1
1
|
import { DyFM_AI_Provider } from '@futdevpro/fsm-dynamo/ai';
|
|
2
2
|
import { DyFM_AI_ProviderCapabilities } from '@futdevpro/fsm-dynamo/ai';
|
|
3
3
|
import { DyFM_AI_Config } from '@futdevpro/fsm-dynamo/ai';
|
|
4
|
+
import { DyFM_Log } from '@futdevpro/fsm-dynamo';
|
|
5
|
+
|
|
6
|
+
import { DyNTS_AI_CostEvent } from '../_models/interfaces/dynts-ai-cost-event.interface';
|
|
7
|
+
import { DyNTS_AI_CostEventCallback } from '../_models/interfaces/dynts-ai-cost-event-callback.interface';
|
|
4
8
|
import { DyNTS_SingletonService } from '../../../_services/base/singleton.service';
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Abstract base class for all AI providers
|
|
8
12
|
* Defines the common interface that all AI providers must implement
|
|
13
|
+
*
|
|
14
|
+
* **Cost-event hook (FR-002)**: a leszármazott concrete service-ek (OAI
|
|
15
|
+
* Embedding / LLM / Chat) a constructor-ban opcionálisan kapnak egy
|
|
16
|
+
* `onCostEvent` callback-et. Minden sikeres AI-call után `emitCostEvent`-tel
|
|
17
|
+
* hívják, ami safe (try/catch) — a callback hibája NEM akasztja meg az
|
|
18
|
+
* AI-call-t. Lásd `DyNTS_AI_CostEvent` interface.
|
|
9
19
|
*/
|
|
10
20
|
export abstract class DyNTS_AI_Provider_ServiceBase extends DyNTS_SingletonService {
|
|
11
21
|
/** The AI provider this service represents */
|
|
12
22
|
abstract readonly aiProvider: DyFM_AI_Provider;
|
|
13
|
-
|
|
23
|
+
|
|
14
24
|
/** The capabilities supported by this provider */
|
|
15
25
|
abstract readonly capabilities: DyFM_AI_ProviderCapabilities;
|
|
16
|
-
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optional per-call cost-event callback (FR-002).
|
|
29
|
+
* A leszármazott service-ek minden sikeres AI-call után meghívják az
|
|
30
|
+
* `emitCostEvent` helper-en keresztül. Non-breaking: ha undefined, nincs emit.
|
|
31
|
+
*/
|
|
32
|
+
protected onCostEvent?: DyNTS_AI_CostEventCallback;
|
|
33
|
+
|
|
17
34
|
/**
|
|
18
35
|
* Initialize the provider with configuration
|
|
19
36
|
* @param config - Provider-specific configuration
|
|
20
37
|
*/
|
|
21
38
|
abstract setup(config: DyFM_AI_Config): void;
|
|
22
|
-
|
|
39
|
+
|
|
23
40
|
/**
|
|
24
41
|
* Test the connection to the AI provider
|
|
25
42
|
* @param issuer - The issuer making the request
|
|
26
43
|
* @returns Promise<boolean> - True if connection is successful
|
|
27
44
|
*/
|
|
28
45
|
abstract testConnection(issuer: string): Promise<boolean>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Safe cost-event emit: ha van regisztrált `onCostEvent` callback, meghívja.
|
|
49
|
+
* A callback hibája NEM propagálódik — try/catch-csel safen hívjuk és
|
|
50
|
+
* `DyFM_Log.warn`-nal jelezzük. Ezzel a consumer-oldali integráció
|
|
51
|
+
* (CCAP `CT_`, telemetry, stb.) hibái nem akasztják meg az AI-call-t.
|
|
52
|
+
*/
|
|
53
|
+
protected emitCostEvent(event: DyNTS_AI_CostEvent): void {
|
|
54
|
+
if (!this.onCostEvent) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
this.onCostEvent(event);
|
|
59
|
+
} catch (err: unknown) {
|
|
60
|
+
const msg: string = err instanceof Error ? err.message : String(err);
|
|
61
|
+
DyFM_Log.warn(
|
|
62
|
+
`DyNTS AI | onCostEvent callback threw (call-type: ${event.callType},`
|
|
63
|
+
+ ` model: ${event.model}, issuer: ${event.issuer}): ${msg}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
29
67
|
}
|
|
@@ -15,5 +15,21 @@ export enum LVS_Search_Mode {
|
|
|
15
15
|
* Eredmények növekvő sorrendben (legkisebb távolság először)
|
|
16
16
|
*/
|
|
17
17
|
l2Distance = 'l2-distance',
|
|
18
|
+
/**
|
|
19
|
+
* Hybrid search: cosine similarity (vektor-fele) + BM25 (text-fele) score-merge.
|
|
20
|
+
*
|
|
21
|
+
* Hasznalat:
|
|
22
|
+
* - `searchMode: LVS_Search_Mode.hybrid`
|
|
23
|
+
* - `textSearchKey: keyof T` — KOTELEZO; melyik string property-n fut a BM25
|
|
24
|
+
* - `hybridWeight?: { vector: number; text: number }` — default { vector: 0.5, text: 0.5 }
|
|
25
|
+
*
|
|
26
|
+
* BM25 min-max normalizalva [0,1] a candidate-szettre (cosine mar 0..1), igy
|
|
27
|
+
* a `weight` direkt linearis kombinacio. Ha minden BM25 score 0 (egyetlen
|
|
28
|
+
* query term sem matchel), a hybrid effektivan cosine-only-re degradalodik.
|
|
29
|
+
*
|
|
30
|
+
* L2+hybrid kombinacio NEM tamogatott — a hybrid mindig cosine-alapu
|
|
31
|
+
* vector-half-fel mukodik.
|
|
32
|
+
*/
|
|
33
|
+
hybrid = 'hybrid',
|
|
18
34
|
}
|
|
19
35
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DyNTS_LVS_BM25_Corpus,
|
|
3
|
+
DyNTS_LVS_BM25_DocScore,
|
|
4
|
+
dyNTS_LVS_BM25_minMaxNormalize,
|
|
5
|
+
} from './lvs-bm25.util';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
describe('| DyNTS_LVS_BM25_Corpus.tokenize', (): void => {
|
|
9
|
+
it('| lowercase + split on \\w+ boundaries', (): void => {
|
|
10
|
+
expect(DyNTS_LVS_BM25_Corpus.tokenize('Hello, World!')).toEqual(['hello', 'world']);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('| identifier marad egy tokenkent (PascalCase)', (): void => {
|
|
14
|
+
expect(DyNTS_LVS_BM25_Corpus.tokenize('UserController')).toEqual(['usercontroller']);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('| hyphenated nev ket tokenre esik', (): void => {
|
|
18
|
+
expect(DyNTS_LVS_BM25_Corpus.tokenize('auth-flow')).toEqual(['auth', 'flow']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('| ures string → ures array', (): void => {
|
|
22
|
+
expect(DyNTS_LVS_BM25_Corpus.tokenize('')).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('| csak whitespace/punctuation → ures array', (): void => {
|
|
26
|
+
expect(DyNTS_LVS_BM25_Corpus.tokenize(' ,.! ')).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('| underscore megmarad (snake_case is egy token)', (): void => {
|
|
30
|
+
expect(DyNTS_LVS_BM25_Corpus.tokenize('user_controller')).toEqual(['user_controller']);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
describe('| DyNTS_LVS_BM25_Corpus.score', (): void => {
|
|
36
|
+
it('| ures corpus → ures array', (): void => {
|
|
37
|
+
const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([]);
|
|
38
|
+
expect(corpus.size()).toBe(0);
|
|
39
|
+
expect(corpus.score('anything')).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('| ures query → minden doc 0 score-t kap', (): void => {
|
|
43
|
+
const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
|
|
44
|
+
{ id: 'a', text: 'foo bar' },
|
|
45
|
+
{ id: 'b', text: 'baz' },
|
|
46
|
+
]);
|
|
47
|
+
const out: DyNTS_LVS_BM25_DocScore[] = corpus.score('');
|
|
48
|
+
expect(out.length).toBe(2);
|
|
49
|
+
expect(out.every((s: DyNTS_LVS_BM25_DocScore) => s.score === 0)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('| query NEM matchelo termmel → minden score 0', (): void => {
|
|
53
|
+
const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
|
|
54
|
+
{ id: 'a', text: 'foo bar' },
|
|
55
|
+
{ id: 'b', text: 'baz' },
|
|
56
|
+
]);
|
|
57
|
+
const out: DyNTS_LVS_BM25_DocScore[] = corpus.score('xyz');
|
|
58
|
+
expect(out.every((s: DyNTS_LVS_BM25_DocScore) => s.score === 0)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('| relevant doc magasabb score-t kap mint nem-relevant', (): void => {
|
|
62
|
+
const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
|
|
63
|
+
{ id: 'a', text: 'the UserController handles authentication' },
|
|
64
|
+
{ id: 'b', text: 'cooking recipes for desserts' },
|
|
65
|
+
{ id: 'c', text: 'database setup guide' },
|
|
66
|
+
]);
|
|
67
|
+
const out: DyNTS_LVS_BM25_DocScore[] = corpus.score('UserController');
|
|
68
|
+
const a: DyNTS_LVS_BM25_DocScore = out.find((s) => s.id === 'a')!;
|
|
69
|
+
const b: DyNTS_LVS_BM25_DocScore = out.find((s) => s.id === 'b')!;
|
|
70
|
+
expect(a.score).toBeGreaterThan(0);
|
|
71
|
+
expect(b.score).toBe(0);
|
|
72
|
+
expect(a.score).toBeGreaterThan(b.score);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('| rarer term (alacsonyabb df) magasabb IDF-et ad → magasabb score', (): void => {
|
|
76
|
+
// 'common' minden docban → alacsony IDF; 'rare' csak az 'a' docban → magas IDF
|
|
77
|
+
const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
|
|
78
|
+
{ id: 'a', text: 'common rare' },
|
|
79
|
+
{ id: 'b', text: 'common' },
|
|
80
|
+
{ id: 'c', text: 'common' },
|
|
81
|
+
{ id: 'd', text: 'common' },
|
|
82
|
+
]);
|
|
83
|
+
const commonOut: DyNTS_LVS_BM25_DocScore[] = corpus.score('common');
|
|
84
|
+
const rareOut: DyNTS_LVS_BM25_DocScore[] = corpus.score('rare');
|
|
85
|
+
const aCommon: number = commonOut.find((s) => s.id === 'a')!.score;
|
|
86
|
+
const aRare: number = rareOut.find((s) => s.id === 'a')!.score;
|
|
87
|
+
expect(aRare).toBeGreaterThan(aCommon);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('| query-term ismetles NEM no monotonan a score-on (term saturation k1)', (): void => {
|
|
91
|
+
// 'a' egy 'foo'-t tartalmaz, 'b' tobbet
|
|
92
|
+
const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
|
|
93
|
+
{ id: 'a', text: 'foo bar baz qux' },
|
|
94
|
+
{ id: 'b', text: 'foo foo foo foo bar baz qux' },
|
|
95
|
+
]);
|
|
96
|
+
const out: DyNTS_LVS_BM25_DocScore[] = corpus.score('foo');
|
|
97
|
+
const a: number = out.find((s) => s.id === 'a')!.score;
|
|
98
|
+
const b: number = out.find((s) => s.id === 'b')!.score;
|
|
99
|
+
// b > a, de NEM 4x a-szor (saturation)
|
|
100
|
+
expect(b).toBeGreaterThan(a);
|
|
101
|
+
expect(b).toBeLessThan(4 * a);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('| case-insensitive — uppercase query matchel lowercase doc-ot', (): void => {
|
|
105
|
+
const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
|
|
106
|
+
{ id: 'a', text: 'usercontroller is great' },
|
|
107
|
+
]);
|
|
108
|
+
const out: DyNTS_LVS_BM25_DocScore[] = corpus.score('USERCONTROLLER');
|
|
109
|
+
expect(out[0].score).toBeGreaterThan(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('| ket query term aggregalja az IDF-eket additivan', (): void => {
|
|
113
|
+
const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
|
|
114
|
+
{ id: 'a', text: 'auth flow handler' },
|
|
115
|
+
{ id: 'b', text: 'auth only' },
|
|
116
|
+
{ id: 'c', text: 'flow only' },
|
|
117
|
+
]);
|
|
118
|
+
const both: number = corpus.score('auth flow').find((s) => s.id === 'a')!.score;
|
|
119
|
+
const authOnly: number = corpus.score('auth').find((s) => s.id === 'a')!.score;
|
|
120
|
+
expect(both).toBeGreaterThan(authOnly);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('| invalid input doc (null/missing id/text) silently skipd', (): void => {
|
|
124
|
+
const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
|
|
125
|
+
{ id: 'a', text: 'hello' },
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
|
+
null as any,
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
{ id: 'b' } as any,
|
|
130
|
+
]);
|
|
131
|
+
expect(corpus.size()).toBe(1);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
describe('| dyNTS_LVS_BM25_minMaxNormalize', (): void => {
|
|
137
|
+
it('| ures input → ures array', (): void => {
|
|
138
|
+
expect(dyNTS_LVS_BM25_minMaxNormalize([])).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('| min 0, max 10 → normalizalva [0..1]', (): void => {
|
|
142
|
+
const out: DyNTS_LVS_BM25_DocScore[] = dyNTS_LVS_BM25_minMaxNormalize([
|
|
143
|
+
{ id: 'a', score: 0 },
|
|
144
|
+
{ id: 'b', score: 5 },
|
|
145
|
+
{ id: 'c', score: 10 },
|
|
146
|
+
]);
|
|
147
|
+
expect(out.find((s) => s.id === 'a')!.score).toBeCloseTo(0, 5);
|
|
148
|
+
expect(out.find((s) => s.id === 'b')!.score).toBeCloseTo(0.5, 5);
|
|
149
|
+
expect(out.find((s) => s.id === 'c')!.score).toBeCloseTo(1, 5);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('| azonos score → minden 0 (NEM 0.5, nincs jel diszkriminaciora)', (): void => {
|
|
153
|
+
const out: DyNTS_LVS_BM25_DocScore[] = dyNTS_LVS_BM25_minMaxNormalize([
|
|
154
|
+
{ id: 'a', score: 3 },
|
|
155
|
+
{ id: 'b', score: 3 },
|
|
156
|
+
]);
|
|
157
|
+
expect(out.every((s) => s.score === 0)).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
});
|