@futdevpro/nts-dynamo 1.15.57 → 1.15.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/.dynamo/logs/cicd-pipeline/output.log +1637 -3567
  2. package/.dynamo/logs/cicd-pipeline/status.json +42 -344
  3. package/build/_modules/ai/_modules/document-ai/_collections/dai-code-chunking.util.d.ts +110 -0
  4. package/build/_modules/ai/_modules/document-ai/_collections/dai-code-chunking.util.d.ts.map +1 -0
  5. package/build/_modules/ai/_modules/document-ai/_collections/dai-code-chunking.util.js +419 -0
  6. package/build/_modules/ai/_modules/document-ai/_collections/dai-code-chunking.util.js.map +1 -0
  7. package/build/_modules/ai/_modules/document-ai/_models/interfaces/dai-code-chunk.interface.d.ts +50 -0
  8. package/build/_modules/ai/_modules/document-ai/_models/interfaces/dai-code-chunk.interface.d.ts.map +1 -0
  9. package/build/_modules/ai/_modules/document-ai/_models/interfaces/dai-code-chunk.interface.js +3 -0
  10. package/build/_modules/ai/_modules/document-ai/_models/interfaces/dai-code-chunk.interface.js.map +1 -0
  11. package/build/_modules/ai/_modules/document-ai/index.d.ts +2 -0
  12. package/build/_modules/ai/_modules/document-ai/index.d.ts.map +1 -1
  13. package/build/_modules/ai/_modules/document-ai/index.js +2 -0
  14. package/build/_modules/ai/_modules/document-ai/index.js.map +1 -1
  15. package/build/_modules/ai/_services/ai-embedding-mock.service.d.ts +81 -0
  16. package/build/_modules/ai/_services/ai-embedding-mock.service.d.ts.map +1 -0
  17. package/build/_modules/ai/_services/ai-embedding-mock.service.js +167 -0
  18. package/build/_modules/ai/_services/ai-embedding-mock.service.js.map +1 -0
  19. package/build/_modules/ai/_services/ai-embedding-provider.registry.d.ts +52 -0
  20. package/build/_modules/ai/_services/ai-embedding-provider.registry.d.ts.map +1 -0
  21. package/build/_modules/ai/_services/ai-embedding-provider.registry.js +79 -0
  22. package/build/_modules/ai/_services/ai-embedding-provider.registry.js.map +1 -0
  23. package/build/_modules/ai/_services/lmstudio-embedding.control-service.d.ts +111 -0
  24. package/build/_modules/ai/_services/lmstudio-embedding.control-service.d.ts.map +1 -0
  25. package/build/_modules/ai/_services/lmstudio-embedding.control-service.js +298 -0
  26. package/build/_modules/ai/_services/lmstudio-embedding.control-service.js.map +1 -0
  27. package/build/_modules/ai/index.d.ts +5 -0
  28. package/build/_modules/ai/index.d.ts.map +1 -1
  29. package/build/_modules/ai/index.js +8 -0
  30. package/build/_modules/ai/index.js.map +1 -1
  31. package/build/_modules/data-readers/_collections/dynts-sqlite-reader.util.d.ts +59 -0
  32. package/build/_modules/data-readers/_collections/dynts-sqlite-reader.util.d.ts.map +1 -0
  33. package/build/_modules/data-readers/_collections/dynts-sqlite-reader.util.js +169 -0
  34. package/build/_modules/data-readers/_collections/dynts-sqlite-reader.util.js.map +1 -0
  35. package/build/_modules/data-readers/_models/interfaces/dynts-sqlite-reader.interface.d.ts +32 -0
  36. package/build/_modules/data-readers/_models/interfaces/dynts-sqlite-reader.interface.d.ts.map +1 -0
  37. package/build/_modules/data-readers/_models/interfaces/dynts-sqlite-reader.interface.js +8 -0
  38. package/build/_modules/data-readers/_models/interfaces/dynts-sqlite-reader.interface.js.map +1 -0
  39. package/build/_modules/data-readers/index.d.ts +3 -0
  40. package/build/_modules/data-readers/index.d.ts.map +1 -0
  41. package/build/_modules/data-readers/index.js +11 -0
  42. package/build/_modules/data-readers/index.js.map +1 -0
  43. package/build/_modules/local-vector-search/_models/data-models/lvs-vector-persist.data-model.d.ts +36 -0
  44. package/build/_modules/local-vector-search/_models/data-models/lvs-vector-persist.data-model.d.ts.map +1 -0
  45. package/build/_modules/local-vector-search/_models/data-models/lvs-vector-persist.data-model.js +54 -0
  46. package/build/_modules/local-vector-search/_models/data-models/lvs-vector-persist.data-model.js.map +1 -0
  47. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.d.ts +70 -0
  48. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.d.ts.map +1 -0
  49. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.js +123 -0
  50. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.js.map +1 -0
  51. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.d.ts +43 -0
  52. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.d.ts.map +1 -0
  53. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.js +72 -0
  54. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.js.map +1 -0
  55. package/build/_modules/local-vector-search/index.d.ts +3 -0
  56. package/build/_modules/local-vector-search/index.d.ts.map +1 -1
  57. package/build/_modules/local-vector-search/index.js +4 -0
  58. package/build/_modules/local-vector-search/index.js.map +1 -1
  59. package/build/_modules/mcp/_models/interfaces/dynts-mcp.interface.d.ts +109 -0
  60. package/build/_modules/mcp/_models/interfaces/dynts-mcp.interface.d.ts.map +1 -0
  61. package/build/_modules/mcp/_models/interfaces/dynts-mcp.interface.js +14 -0
  62. package/build/_modules/mcp/_models/interfaces/dynts-mcp.interface.js.map +1 -0
  63. package/build/_modules/mcp/_services/dynts-mcp-server.service-base.d.ts +71 -0
  64. package/build/_modules/mcp/_services/dynts-mcp-server.service-base.d.ts.map +1 -0
  65. package/build/_modules/mcp/_services/dynts-mcp-server.service-base.js +99 -0
  66. package/build/_modules/mcp/_services/dynts-mcp-server.service-base.js.map +1 -0
  67. package/build/_modules/mcp/_services/dynts-mcp.adapter.d.ts +57 -0
  68. package/build/_modules/mcp/_services/dynts-mcp.adapter.d.ts.map +1 -0
  69. package/build/_modules/mcp/_services/dynts-mcp.adapter.js +139 -0
  70. package/build/_modules/mcp/_services/dynts-mcp.adapter.js.map +1 -0
  71. package/build/_modules/mcp/index.d.ts +4 -0
  72. package/build/_modules/mcp/index.d.ts.map +1 -0
  73. package/build/_modules/mcp/index.js +13 -0
  74. package/build/_modules/mcp/index.js.map +1 -0
  75. package/build/_modules/scoped-config/_enums/dynts-scoped-config-level.enum.d.ts +19 -0
  76. package/build/_modules/scoped-config/_enums/dynts-scoped-config-level.enum.d.ts.map +1 -0
  77. package/build/_modules/scoped-config/_enums/dynts-scoped-config-level.enum.js +23 -0
  78. package/build/_modules/scoped-config/_enums/dynts-scoped-config-level.enum.js.map +1 -0
  79. package/build/_modules/scoped-config/_models/data-models/dynts-scoped-config.data-model.d.ts +44 -0
  80. package/build/_modules/scoped-config/_models/data-models/dynts-scoped-config.data-model.d.ts.map +1 -0
  81. package/build/_modules/scoped-config/_models/data-models/dynts-scoped-config.data-model.js +68 -0
  82. package/build/_modules/scoped-config/_models/data-models/dynts-scoped-config.data-model.js.map +1 -0
  83. package/build/_modules/scoped-config/_models/interfaces/dynts-scoped-config.interface.d.ts +89 -0
  84. package/build/_modules/scoped-config/_models/interfaces/dynts-scoped-config.interface.d.ts.map +1 -0
  85. package/build/_modules/scoped-config/_models/interfaces/dynts-scoped-config.interface.js +12 -0
  86. package/build/_modules/scoped-config/_models/interfaces/dynts-scoped-config.interface.js.map +1 -0
  87. package/build/_modules/scoped-config/_services/dynts-scoped-config.control-service.d.ts +84 -0
  88. package/build/_modules/scoped-config/_services/dynts-scoped-config.control-service.d.ts.map +1 -0
  89. package/build/_modules/scoped-config/_services/dynts-scoped-config.control-service.js +220 -0
  90. package/build/_modules/scoped-config/_services/dynts-scoped-config.control-service.js.map +1 -0
  91. package/build/_modules/scoped-config/_services/dynts-scoped-config.data-service.d.ts +54 -0
  92. package/build/_modules/scoped-config/_services/dynts-scoped-config.data-service.d.ts.map +1 -0
  93. package/build/_modules/scoped-config/_services/dynts-scoped-config.data-service.js +76 -0
  94. package/build/_modules/scoped-config/_services/dynts-scoped-config.data-service.js.map +1 -0
  95. package/build/_modules/scoped-config/index.d.ts +6 -0
  96. package/build/_modules/scoped-config/index.d.ts.map +1 -0
  97. package/build/_modules/scoped-config/index.js +15 -0
  98. package/build/_modules/scoped-config/index.js.map +1 -0
  99. package/package.json +58 -2
  100. package/pnpm-workspace.yaml +1 -0
  101. package/src/_modules/ai/_modules/document-ai/_collections/dai-code-chunking.util.spec.ts +295 -0
  102. package/src/_modules/ai/_modules/document-ai/_collections/dai-code-chunking.util.ts +552 -0
  103. package/src/_modules/ai/_modules/document-ai/_models/interfaces/dai-code-chunk.interface.ts +68 -0
  104. package/src/_modules/ai/_modules/document-ai/index.ts +2 -0
  105. package/src/_modules/ai/_services/ai-embedding-mock.service.spec.ts +115 -0
  106. package/src/_modules/ai/_services/ai-embedding-mock.service.ts +233 -0
  107. package/src/_modules/ai/_services/ai-embedding-provider.registry.spec.ts +110 -0
  108. package/src/_modules/ai/_services/ai-embedding-provider.registry.ts +114 -0
  109. package/src/_modules/ai/_services/lmstudio-embedding.control-service.spec.ts +197 -0
  110. package/src/_modules/ai/_services/lmstudio-embedding.control-service.ts +399 -0
  111. package/src/_modules/ai/index.ts +10 -0
  112. package/src/_modules/data-readers/_collections/dynts-sqlite-reader.util.spec.ts +176 -0
  113. package/src/_modules/data-readers/_collections/dynts-sqlite-reader.util.ts +203 -0
  114. package/src/_modules/data-readers/_models/interfaces/dynts-sqlite-reader.interface.ts +33 -0
  115. package/src/_modules/data-readers/index.ts +11 -0
  116. package/src/_modules/local-vector-search/_models/data-models/lvs-vector-persist.data-model.ts +60 -0
  117. package/src/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.spec.ts +198 -0
  118. package/src/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.ts +150 -0
  119. package/src/_modules/local-vector-search/_services/lvs-vector-persist.data-service.spec.ts +167 -0
  120. package/src/_modules/local-vector-search/_services/lvs-vector-persist.data-service.ts +108 -0
  121. package/src/_modules/local-vector-search/index.ts +6 -1
  122. package/src/_modules/mcp/_models/interfaces/dynts-mcp.interface.ts +111 -0
  123. package/src/_modules/mcp/_services/dynts-mcp-server.service-base.spec.ts +151 -0
  124. package/src/_modules/mcp/_services/dynts-mcp-server.service-base.ts +125 -0
  125. package/src/_modules/mcp/_services/dynts-mcp.adapter.ts +168 -0
  126. package/src/_modules/mcp/index.ts +13 -0
  127. package/src/_modules/scoped-config/_enums/dynts-scoped-config-level.enum.ts +22 -0
  128. package/src/_modules/scoped-config/_models/data-models/dynts-scoped-config.data-model.ts +82 -0
  129. package/src/_modules/scoped-config/_models/interfaces/dynts-scoped-config.interface.ts +107 -0
  130. package/src/_modules/scoped-config/_services/dynts-scoped-config.control-service.spec.ts +312 -0
  131. package/src/_modules/scoped-config/_services/dynts-scoped-config.control-service.ts +311 -0
  132. package/src/_modules/scoped-config/_services/dynts-scoped-config.data-service.spec.ts +123 -0
  133. package/src/_modules/scoped-config/_services/dynts-scoped-config.data-service.ts +108 -0
  134. package/src/_modules/scoped-config/index.ts +17 -0
@@ -0,0 +1,197 @@
1
+ import { CreateEmbeddingResponse } from 'openai/resources';
2
+ import { DyFM_AI_Provider } from '@futdevpro/fsm-dynamo/ai';
3
+ import { DyFM_Error } from '@futdevpro/fsm-dynamo';
4
+
5
+ import { DyNTS_LMStudio_Embedding_ControlService } from './lmstudio-embedding.control-service';
6
+ import { DyNTS_AI_CostEvent } from '../_models/interfaces/dynts-ai-cost-event.interface';
7
+
8
+ describe('| DyNTS_LMStudio_Embedding_ControlService', () => {
9
+
10
+ const baseUrl: string = 'http://localhost:1234/v1';
11
+ const issuer: string = 'test-issuer';
12
+
13
+ /** Egy OpenAI-kompatibilis embeddings-válasz JSON-string-é csomagolva, fetch-mockhoz. */
14
+ function okResponse(embeddings: number[][], usage?: { prompt_tokens: number; total_tokens: number }): Response {
15
+ const body: string = JSON.stringify({
16
+ object: 'list',
17
+ data: embeddings.map((embedding, index) => ({ object: 'embedding', embedding: embedding, index: index })),
18
+ usage: usage,
19
+ });
20
+ return {
21
+ ok: true,
22
+ status: 200,
23
+ text: () => Promise.resolve(body),
24
+ } as Response;
25
+ }
26
+
27
+ /** Egy hibás (non-2xx) válasz fetch-mockhoz. */
28
+ function errorResponse(status: number, body: string): Response {
29
+ return {
30
+ ok: false,
31
+ status: status,
32
+ text: () => Promise.resolve(body),
33
+ } as Response;
34
+ }
35
+
36
+ let fetchSpy: jasmine.Spy;
37
+
38
+ beforeEach(() => {
39
+ fetchSpy = spyOn(globalThis, 'fetch');
40
+ });
41
+
42
+ describe('| constructor', () => {
43
+ it('| should throw a DyFM_Error if baseUrl is empty', () => {
44
+ expect(() => new DyNTS_LMStudio_Embedding_ControlService({ baseUrl: '' })).toThrowMatching(
45
+ (error: unknown) => error instanceof DyFM_Error,
46
+ );
47
+ });
48
+
49
+ it('| should set provider to LocalAI', () => {
50
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({ baseUrl: baseUrl });
51
+ expect(service.aiProvider).toBe(DyFM_AI_Provider.LocalAI);
52
+ expect(service.capabilities.embeddings).toBe(true);
53
+ });
54
+ });
55
+
56
+ describe('| createEmbeddings', () => {
57
+ it('| should POST to ${baseUrl}/embeddings and return vectors in order', async () => {
58
+ fetchSpy.and.returnValue(Promise.resolve(okResponse([[0.1, 0.2], [0.3, 0.4]])));
59
+
60
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({ baseUrl: baseUrl });
61
+ const result: number[][] | CreateEmbeddingResponse = await service.createEmbeddings({
62
+ texts: ['hello', 'world'],
63
+ model: 'nomic-embed-text-v1.5',
64
+ issuer: issuer,
65
+ });
66
+
67
+ expect(result).toEqual([[0.1, 0.2], [0.3, 0.4]]);
68
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
69
+ const url: string = fetchSpy.calls.mostRecent().args[0] as string;
70
+ expect(url).toBe('http://localhost:1234/v1/embeddings');
71
+ const init: RequestInit = fetchSpy.calls.mostRecent().args[1] as RequestInit;
72
+ expect(init.method).toBe('POST');
73
+ const sentBody: { model: string; input: string[] } = JSON.parse(init.body as string);
74
+ expect(sentBody.model).toBe('nomic-embed-text-v1.5');
75
+ expect(sentBody.input).toEqual(['hello', 'world']);
76
+ });
77
+
78
+ it('| should trim trailing slashes from baseUrl', async () => {
79
+ fetchSpy.and.returnValue(Promise.resolve(okResponse([[0.5]])));
80
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({
81
+ baseUrl: 'http://localhost:1234/v1///',
82
+ });
83
+ await service.createEmbeddings({ texts: ['x'], model: 'm', issuer: issuer });
84
+ expect(fetchSpy.calls.mostRecent().args[0]).toBe('http://localhost:1234/v1/embeddings');
85
+ });
86
+
87
+ it('| should add a Bearer header when apiKey is provided', async () => {
88
+ fetchSpy.and.returnValue(Promise.resolve(okResponse([[0.5]])));
89
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({
90
+ baseUrl: baseUrl,
91
+ apiKey: 'secret-token',
92
+ });
93
+ await service.createEmbeddings({ texts: ['x'], model: 'm', issuer: issuer });
94
+ const init: RequestInit = fetchSpy.calls.mostRecent().args[1] as RequestInit;
95
+ const headers: { [key: string]: string } = init.headers as { [key: string]: string };
96
+ expect(headers.Authorization).toBe('Bearer secret-token');
97
+ });
98
+
99
+ it('| should throw a DyFM_Error on a non-ok HTTP response', async () => {
100
+ fetchSpy.and.returnValue(Promise.resolve(errorResponse(500, 'boom')));
101
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({ baseUrl: baseUrl });
102
+
103
+ await expectAsync(
104
+ service.createEmbeddings({ texts: ['x'], model: 'm', issuer: issuer }),
105
+ ).toBeRejectedWith(jasmine.any(DyFM_Error));
106
+ });
107
+
108
+ it('| should throw a DyFM_Error if the vector count does not match the text count', async () => {
109
+ fetchSpy.and.returnValue(Promise.resolve(okResponse([[0.1]])));
110
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({ baseUrl: baseUrl });
111
+
112
+ await expectAsync(
113
+ service.createEmbeddings({ texts: ['a', 'b'], model: 'm', issuer: issuer }),
114
+ ).toBeRejectedWith(jasmine.any(DyFM_Error));
115
+ });
116
+
117
+ it('| should emit an embedding-batch cost-event with provider lm-studio', async () => {
118
+ fetchSpy.and.returnValue(Promise.resolve(okResponse([[0.1], [0.2]], { prompt_tokens: 7, total_tokens: 7 })));
119
+ const events: DyNTS_AI_CostEvent[] = [];
120
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({
121
+ baseUrl: baseUrl,
122
+ onCostEvent: (event: DyNTS_AI_CostEvent) => events.push(event),
123
+ });
124
+
125
+ await service.createEmbeddings({ texts: ['a', 'b'], model: 'm', issuer: issuer });
126
+
127
+ expect(events.length).toBe(1);
128
+ expect(events[0].callType).toBe('embedding-batch');
129
+ expect(events[0].provider).toBe('lm-studio');
130
+ expect(events[0].model).toBe('m');
131
+ expect(events[0].tokensUsed.input).toBe(7);
132
+ expect(events[0].issuer).toBe(issuer);
133
+ });
134
+
135
+ it('| should estimate tokens when the endpoint provides no usage', async () => {
136
+ fetchSpy.and.returnValue(Promise.resolve(okResponse([[0.1]])));
137
+ const events: DyNTS_AI_CostEvent[] = [];
138
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({
139
+ baseUrl: baseUrl,
140
+ onCostEvent: (event: DyNTS_AI_CostEvent) => events.push(event),
141
+ });
142
+
143
+ await service.createEmbeddings({ texts: ['abcdefgh'], model: 'm', issuer: issuer });
144
+
145
+ // 8 chars / 4 = 2 estimated tokens.
146
+ expect(events[0].tokensUsed.input).toBe(2);
147
+ });
148
+ });
149
+
150
+ describe('| createEmbedding (single)', () => {
151
+ it('| should emit an embedding-single cost-event and return a single vector', async () => {
152
+ fetchSpy.and.returnValue(Promise.resolve(okResponse([[0.9, 0.8, 0.7]], { prompt_tokens: 3, total_tokens: 3 })));
153
+ const events: DyNTS_AI_CostEvent[] = [];
154
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({
155
+ baseUrl: baseUrl,
156
+ onCostEvent: (event: DyNTS_AI_CostEvent) => events.push(event),
157
+ });
158
+
159
+ const result: number[] | CreateEmbeddingResponse = await service.createEmbedding({
160
+ text: 'one',
161
+ model: 'm',
162
+ issuer: issuer,
163
+ });
164
+
165
+ expect(result).toEqual([0.9, 0.8, 0.7]);
166
+ expect(events[0].callType).toBe('embedding-single');
167
+ });
168
+
169
+ it('| should throw a DyFM_Error if text is empty', async () => {
170
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({ baseUrl: baseUrl });
171
+ await expectAsync(
172
+ service.createEmbedding({ text: '', model: 'm', issuer: issuer }),
173
+ ).toBeRejectedWith(jasmine.any(DyFM_Error));
174
+ });
175
+ });
176
+
177
+ describe('| testConnection', () => {
178
+ it('| should return true when the probe succeeds', async () => {
179
+ fetchSpy.and.returnValue(Promise.resolve(okResponse([[0.1, 0.2]])));
180
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({ baseUrl: baseUrl });
181
+ expect(await service.testConnection(issuer)).toBe(true);
182
+ });
183
+
184
+ it('| should return false (never throw) when the probe fails', async () => {
185
+ fetchSpy.and.returnValue(Promise.reject(new Error('connection refused')));
186
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({ baseUrl: baseUrl });
187
+ expect(await service.testConnection(issuer)).toBe(false);
188
+ });
189
+ });
190
+
191
+ describe('| getEmbeddingInfo', () => {
192
+ it('| should report the LocalAI provider and the given model', () => {
193
+ const service: DyNTS_LMStudio_Embedding_ControlService = new DyNTS_LMStudio_Embedding_ControlService({ baseUrl: baseUrl });
194
+ expect(service.getEmbeddingInfo('my-model')).toEqual({ provider: DyFM_AI_Provider.LocalAI, model: 'my-model' });
195
+ });
196
+ });
197
+ });
@@ -0,0 +1,399 @@
1
+ import { CreateEmbeddingResponse } from 'openai/resources';
2
+
3
+ import { DyFM_Error, DyFM_Log } from '@futdevpro/fsm-dynamo';
4
+ import { DyFM_AI_Provider, DyFM_AI_ProviderCapabilities, DyFM_AI_Config } from '@futdevpro/fsm-dynamo/ai';
5
+ import { DyFM_DAI_EmbeddingInfo } from '@futdevpro/fsm-dynamo/ai/document-ai';
6
+
7
+ import { DyNTS_AI_CostEventCallback } from '../_models/interfaces/dynts-ai-cost-event-callback.interface';
8
+ import { DyNTS_global_settings } from '../../../_collections/global-settings.const';
9
+ import { DyNTS_AI_Embedding_ServiceBase } from './ai-embedding.service-base';
10
+
11
+ /**
12
+ * Az LM Studio embedding control-service config-set-je. A `baseUrl` az OpenAI-kompatibilis
13
+ * lokális endpoint (pl. `http://localhost:1234/v1`), az `apiKey` opcionális (LM Studio default-
14
+ * ban nem kér Bearer-t). Az `onCostEvent` a per-call cost-event sink (FR-002, BFR-AM-007).
15
+ */
16
+ export interface DyNTS_LMStudio_Embedding_Settings {
17
+ /** OpenAI-kompatibilis lokális embedding-endpoint base URL-je (pl. `http://localhost:1234/v1`). */
18
+ baseUrl: string;
19
+ /** Opcionális Bearer token (LM Studio default-ban nem kér). */
20
+ apiKey?: string;
21
+ /** Per-call cost-event callback (FR-002 / BFR-AM-007). Non-breaking: ha undefined, nincs emit. */
22
+ onCostEvent?: DyNTS_AI_CostEventCallback;
23
+ }
24
+
25
+ /**
26
+ * `DyNTS_LMStudio_Embedding_ControlService` (BFR-AM-002) — OpenAI-kompatibilis **lokális** embedding
27
+ * adapter `fetch`-en (Node 20 global `fetch`, így a Dynamo NEM hoz be provider-specifikus SDK-t a
28
+ * lokális path-hoz). A FAM `FAM_LMStudio_EmbeddingProvider` workaround-ját emeli bedrock-szintre.
29
+ *
30
+ * A `baseUrl` + `modelId` config-ot a constructor-on kapja (mint a `DyNTS_OAI_Embedding_ControlService`
31
+ * a `DyFM_OAI_Settings`-et). A `${baseUrl}/embeddings` POST-ra a `{ model, input }` body-t küldi, a
32
+ * `data[].embedding` sorrendje == a `texts` sorrendje.
33
+ *
34
+ * **Cost-event (BFR-AM-007):** minden sikeres call után `emitCostEvent`-tel jelez (callType
35
+ * `embedding-single` / `embedding-batch`, provider `'lm-studio'`). Lokális futás → nincs USD-költség,
36
+ * a token-fogyasztás a `usage`-ből (ha az endpoint adja), különben becsült (4 char ≈ 1 token).
37
+ */
38
+ export class DyNTS_LMStudio_Embedding_ControlService extends DyNTS_AI_Embedding_ServiceBase {
39
+
40
+ /** A provider-azonosító a `LocalAI` enum-érték (LM Studio = lokális OpenAI-kompatibilis endpoint). */
41
+ readonly aiProvider: DyFM_AI_Provider = DyFM_AI_Provider.LocalAI;
42
+
43
+ /** A cost-event provider-string (a `DyNTS_AI_CostEvent.provider` szabad-string mezőjéhez). */
44
+ protected readonly costProvider: string = 'lm-studio';
45
+
46
+ /** LM Studio (OpenAI-kompatibilis lokális) capability-k: csak embedding-et igénylünk innen. */
47
+ readonly capabilities: DyFM_AI_ProviderCapabilities = {
48
+ chat: false,
49
+ embeddings: true,
50
+ imageGeneration: false,
51
+ vision: false,
52
+ audioGeneration: false,
53
+ audioAnalysis: false,
54
+ functionCalling: false,
55
+ streaming: false,
56
+ batchOperations: true,
57
+ supportedModelTypes: [],
58
+ };
59
+
60
+ /** Az OpenAI-kompatibilis endpoint base URL-je (trailing slash-mentes). */
61
+ protected baseUrl: string;
62
+
63
+ /** Opcionális Bearer token. */
64
+ protected apiKey?: string;
65
+
66
+ /**
67
+ * @param set baseUrl + opc. apiKey + opc. onCostEvent. A `baseUrl` kötelező — ha üres, a
68
+ * `DyNTS-LMS-ECS-CFG` hibát dobjuk (lokális endpoint nincs konfigurálva).
69
+ */
70
+ constructor(set: DyNTS_LMStudio_Embedding_Settings) {
71
+ super();
72
+
73
+ if (!set?.baseUrl?.trim().length) {
74
+ throw new DyFM_Error({
75
+ ...this.getDefaultErrorSettings(
76
+ 'constructor',
77
+ new Error('LM Studio baseUrl is required (OpenAI-compatible local endpoint)'),
78
+ 'DyNTS_LMStudio_Embedding_ControlService',
79
+ ),
80
+ errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-LMS-ECS-CFG`,
81
+ });
82
+ }
83
+ this.baseUrl = this.trimTrailingSlashes(set.baseUrl.trim());
84
+ this.apiKey = set.apiKey;
85
+
86
+ if (set.onCostEvent) {
87
+ this.onCostEvent = set.onCostEvent;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Az LM Studio client-et nem SDK-val, hanem `fetch`-csel hívjuk; a `setup` a `baseUrl` (és
93
+ * opcionálisan az `apiKey`) átkonfigurálását teszi lehetővé (a base-szerződés egységessége miatt).
94
+ */
95
+ setup(config: DyFM_AI_Config): void {
96
+ if (config?.baseURL) {
97
+ this.baseUrl = this.trimTrailingSlashes(config.baseURL);
98
+ }
99
+
100
+ if (config?.apiKey) {
101
+ this.apiKey = config.apiKey;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Egy-szöveg embedding (a `createEmbeddings` egyelemű alakja). `fullResponse=true` esetén a
107
+ * teljes (OpenAI-alakú) válasz-objektumot adja vissza, különben a tiszta `number[]`-t.
108
+ */
109
+ async createEmbedding(
110
+ set: {
111
+ text: string;
112
+ model: string;
113
+ fullResponse?: boolean;
114
+ issuer: string;
115
+ }
116
+ ): Promise<number[] | CreateEmbeddingResponse> {
117
+ try {
118
+ if (!set.text) {
119
+ throw new DyFM_Error({
120
+ ...this.getDefaultErrorSettings('createEmbedding', new Error('text is required'), set.issuer),
121
+ errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-LMS-ECS-CE1`,
122
+ });
123
+ }
124
+
125
+ const start: number = Date.now();
126
+ const response: CreateEmbeddingResponse = await this.callEmbeddingsEndpoint(set.model, set.text, set.issuer);
127
+ const durationMs: number = Date.now() - start;
128
+
129
+ this.emitLmStudioCostEvent('embedding-single', set.model, [ set.text ], response, durationMs, set.issuer);
130
+
131
+ if (set.fullResponse) {
132
+ return response;
133
+ }
134
+
135
+ return response.data[0].embedding;
136
+ } catch (error) {
137
+ if (error instanceof DyFM_Error) {
138
+ throw error;
139
+ }
140
+
141
+ throw new DyFM_Error({
142
+ ...this.getDefaultErrorSettings('createEmbedding', error, set.issuer),
143
+ errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-LMS-ECS-CE0`,
144
+ });
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Batch-embedding az OpenAI-kompatibilis `/embeddings` végpontra. A `texts` sorrendje == a
150
+ * visszaadott `number[][]` sorrendje. `fullResponse=true` esetén a teljes válasz-objektum.
151
+ */
152
+ async createEmbeddings(
153
+ set: {
154
+ texts: string[];
155
+ model: string;
156
+ fullResponse?: boolean;
157
+ issuer: string;
158
+ }
159
+ ): Promise<number[][] | CreateEmbeddingResponse> {
160
+ try {
161
+ if (!set.texts) {
162
+ throw new DyFM_Error({
163
+ ...this.getDefaultErrorSettings('createEmbeddings', new Error('texts is required'), set.issuer),
164
+ errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-LMS-ECS-CES1`,
165
+ });
166
+ }
167
+
168
+ const start: number = Date.now();
169
+ const response: CreateEmbeddingResponse = await this.callEmbeddingsEndpoint(set.model, set.texts, set.issuer);
170
+ const durationMs: number = Date.now() - start;
171
+
172
+ this.emitLmStudioCostEvent('embedding-batch', set.model, set.texts, response, durationMs, set.issuer);
173
+
174
+ if (set.fullResponse) {
175
+ return response;
176
+ }
177
+
178
+ return response.data.map((item) => item.embedding);
179
+ } catch (error) {
180
+ if (error instanceof DyFM_Error) {
181
+ throw error;
182
+ }
183
+
184
+ throw new DyFM_Error({
185
+ ...this.getDefaultErrorSettings('createEmbeddings', error, set.issuer),
186
+ errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-LMS-ECS-CES0`,
187
+ });
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Embedding model-info (a `DyFM_DAI_EmbeddingInfo` szerződés szerint). A provider `LocalAI`, a
193
+ * model a megadott azonosító (a lokális endpoint natív dimenzióját nem ismerjük előre).
194
+ */
195
+ getEmbeddingInfo(model: string): DyFM_DAI_EmbeddingInfo {
196
+ return {
197
+ provider: DyFM_AI_Provider.LocalAI,
198
+ model: model,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Provider-elérhetőség: egy minimál embedding-próba (rövid token). Ha a hívás dob (endpoint down /
204
+ * model hiányzik), `false`. Soha nem propagál hibát (try/catch → false).
205
+ */
206
+ async testConnection(issuer: string): Promise<boolean> {
207
+ try {
208
+ const vectors: number[][] | CreateEmbeddingResponse = await this.createEmbeddings({
209
+ texts: [ 'ping' ],
210
+ model: this.defaultProbeModel(),
211
+ fullResponse: false,
212
+ issuer: issuer,
213
+ });
214
+
215
+ return Array.isArray(vectors) && vectors.length === 1 && vectors[0].length > 0;
216
+ } catch (error) {
217
+ DyFM_Log.error('DyNTS_LMStudio_Embedding_ControlService', 'testConnection', 'Connection test failed', {
218
+ error: error,
219
+ issuer: issuer,
220
+ });
221
+
222
+ return false;
223
+ }
224
+ }
225
+
226
+ // =========================================================================
227
+ // belső: fetch + parse + cost-event
228
+ // =========================================================================
229
+
230
+ /**
231
+ * Az OpenAI-kompatibilis `/embeddings` POST-hívás `fetch`-csel. A `input` lehet egyetlen string
232
+ * vagy string-tömb (mindkettőt az OpenAI-spec megengedi). A választ `CreateEmbeddingResponse`-alakra
233
+ * normalizáljuk (a `data[].embedding`-eket kivonatoljuk). HTTP-/parse-hiba deskriptív üzenettel dobódik.
234
+ */
235
+ protected async callEmbeddingsEndpoint(
236
+ model: string,
237
+ input: string | string[],
238
+ issuer: string,
239
+ ): Promise<CreateEmbeddingResponse> {
240
+ const url: string = `${this.baseUrl}/embeddings`;
241
+ const headers: { [key: string]: string } = { 'Content-Type': 'application/json' };
242
+
243
+ if (this.apiKey && this.apiKey.trim().length) {
244
+ headers.Authorization = `Bearer ${this.apiKey.trim()}`;
245
+ }
246
+
247
+ const response: Response = await fetch(url, {
248
+ method: 'POST',
249
+ headers: headers,
250
+ body: JSON.stringify({ model: model, input: input }),
251
+ });
252
+ const rawText: string = await response.text();
253
+
254
+ if (!response.ok) {
255
+ throw new Error(
256
+ `LM Studio (OpenAI-compatible) embeddings HTTP ${response.status} | url=${url} | `
257
+ + `model=${model} | body=${this.snapshot(rawText)} | issuer=${issuer}`,
258
+ );
259
+ }
260
+
261
+ let parsed: unknown;
262
+
263
+ try {
264
+ parsed = JSON.parse(rawText);
265
+ } catch {
266
+ throw new Error(
267
+ `LM Studio embeddings: non-JSON response | url=${url} | model=${model} | body=${this.snapshot(rawText)}`,
268
+ );
269
+ }
270
+
271
+ const expectedCount: number = Array.isArray(input) ? input.length : 1;
272
+
273
+ return this.normalizeResponse(parsed, expectedCount, url, model);
274
+ }
275
+
276
+ /**
277
+ * A nyers (OpenAI-kompatibilis) választ `CreateEmbeddingResponse`-alakra normalizálja: a `data[]`-ból
278
+ * a `embedding` tömböket emeli ki (csak véges number-eket), a darabszámot ellenőrzi (1:1 a `texts`-szel),
279
+ * és átveszi a `usage`-t ha van (a cost-event token-számához). Hibás/hiányzó mező → deskriptív hiba.
280
+ */
281
+ protected normalizeResponse(json: unknown, expectedCount: number, url: string, modelId: string): CreateEmbeddingResponse {
282
+ if (json === null || typeof json !== 'object' || Array.isArray(json)) {
283
+ throw new Error(`LM Studio embeddings: invalid response object | url=${url} | model=${modelId}`);
284
+ }
285
+ const dataRaw: unknown = Reflect.get(json, 'data');
286
+
287
+ if (!Array.isArray(dataRaw)) {
288
+ throw new Error(`LM Studio embeddings: missing data array | url=${url} | model=${modelId}`);
289
+ }
290
+
291
+ const data: CreateEmbeddingResponse['data'] = [];
292
+ let index: number = 0;
293
+
294
+ for (const item of dataRaw) {
295
+ if (item === null || typeof item !== 'object' || Array.isArray(item)) {
296
+ continue;
297
+ }
298
+ const embRaw: unknown = Reflect.get(item, 'embedding');
299
+
300
+ if (!Array.isArray(embRaw)) {
301
+ continue;
302
+ }
303
+ const vec: number[] = embRaw.filter((x: unknown): x is number => typeof x === 'number' && Number.isFinite(x));
304
+
305
+ if (vec.length) {
306
+ data.push({ object: 'embedding', embedding: vec, index: index });
307
+ index++;
308
+ }
309
+ }
310
+
311
+ if (data.length !== expectedCount) {
312
+ throw new Error(
313
+ `LM Studio embeddings: expected ${expectedCount} vectors, got ${data.length} | url=${url} | model=${modelId}`,
314
+ );
315
+ }
316
+
317
+ const usageRaw: unknown = Reflect.get(json, 'usage');
318
+ const promptTokens: number = this.readNumber(usageRaw, 'prompt_tokens');
319
+ const totalTokens: number = this.readNumber(usageRaw, 'total_tokens');
320
+
321
+ return {
322
+ object: 'list',
323
+ model: modelId,
324
+ data: data,
325
+ usage: {
326
+ prompt_tokens: promptTokens,
327
+ total_tokens: totalTokens || promptTokens,
328
+ },
329
+ };
330
+ }
331
+
332
+ /**
333
+ * Cost-event emit (BFR-AM-007). A token a `usage`-ből (ha az endpoint adta), különben becsült
334
+ * (4 char ≈ 1 token, defenzív lokális heurisztika). A provider mindig `'lm-studio'` string.
335
+ */
336
+ protected emitLmStudioCostEvent(
337
+ callType: 'embedding-single' | 'embedding-batch',
338
+ model: string,
339
+ texts: string[],
340
+ response: CreateEmbeddingResponse,
341
+ durationMs: number,
342
+ issuer: string,
343
+ ): void {
344
+ const reportedInput: number = response.usage?.prompt_tokens ?? 0;
345
+ const input: number = reportedInput || this.estimateTokens(texts);
346
+ const total: number = response.usage?.total_tokens || input;
347
+
348
+ this.emitCostEvent({
349
+ callType: callType,
350
+ provider: this.costProvider,
351
+ model: model,
352
+ tokensUsed: {
353
+ input: input,
354
+ total: total,
355
+ },
356
+ durationMs: durationMs,
357
+ issuer: issuer,
358
+ timestamp: new Date(),
359
+ });
360
+ }
361
+
362
+ /** Becsült token-szám lokális endpoint-hoz (ha nincs `usage`): 4 char ≈ 1 token. */
363
+ protected estimateTokens(texts: string[]): number {
364
+ let chars: number = 0;
365
+
366
+ for (const text of texts) {
367
+ chars += (text ?? '').length;
368
+ }
369
+
370
+ return Math.max(1, Math.ceil(chars / 4));
371
+ }
372
+
373
+ /** A `testConnection` próba-modellje (env-override-olható, default a FAM-mintára). */
374
+ protected defaultProbeModel(): string {
375
+ return process.env.LMSTUDIO_EMBEDDING_MODEL || 'nomic-embed-text-v1.5';
376
+ }
377
+
378
+ /** Trailing `/`-ek levágása (kettős slash elkerülése a `/embeddings` join-nál). */
379
+ protected trimTrailingSlashes(value: string): string {
380
+ return value.replace(/\/+$/, '');
381
+ }
382
+
383
+ /** Rövid, biztonságos válasz-snapshot a hiba-üzenethez (max 300 char). */
384
+ protected snapshot(value: string): string {
385
+ const trimmed: string = (value ?? '').slice(0, 300);
386
+
387
+ return trimmed.length === 300 ? `${trimmed}…` : trimmed;
388
+ }
389
+
390
+ /** Egy number mező típusbiztos kiolvasása egy ismeretlen objektumból (hiány → 0). */
391
+ protected readNumber(obj: unknown, key: string): number {
392
+ if (obj === null || typeof obj !== 'object') {
393
+ return 0;
394
+ }
395
+ const value: unknown = Reflect.get(obj, key);
396
+
397
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
398
+ }
399
+ }
@@ -5,9 +5,19 @@ export * from '@futdevpro/fsm-dynamo/ai';
5
5
  export * from './_models/ai-input-interfaces';
6
6
  export * from './_models/ai-test-generation-result.interface';
7
7
 
8
+ // Cost-event interfaces (BFR-AM-007) — a consumer (CCAP / FAM) ezekkel típusozza az `onCostEvent`
9
+ // callback-jét; ezért barrel-export kell (M3, FAM-REV bedrock-fix).
10
+ export * from './_models/interfaces/dynts-ai-cost-event.interface';
11
+ export * from './_models/interfaces/dynts-ai-cost-event-callback.interface';
12
+
8
13
  // Abstract Services
9
14
  export * from './_services/ai-provider.service-base';
10
15
  export * from './_services/ai-llm.service-base';
11
16
  export * from './_services/ai-llm-chat.service-base';
12
17
  export * from './_services/ai-embedding.service-base';
13
18
  export * from './_services/ai-user-key.service-base';
19
+
20
+ // Concrete Embedding Services + provider-dispatch (BFR-AM-002 / 007 / 008)
21
+ export * from './_services/lmstudio-embedding.control-service';
22
+ export * from './_services/ai-embedding-mock.service';
23
+ export * from './_services/ai-embedding-provider.registry';