@multiplayer-app/ai-agent-node 0.0.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/.env.example +45 -0
- package/README.md +611 -0
- package/config.example.json +73 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/helpers/AIHelper.d.ts +23 -0
- package/dist/helpers/AIHelper.d.ts.map +1 -0
- package/dist/helpers/AIHelper.js +326 -0
- package/dist/helpers/AIHelper.js.map +1 -0
- package/dist/helpers/AIHelper.test.d.ts +2 -0
- package/dist/helpers/AIHelper.test.d.ts.map +1 -0
- package/dist/helpers/AIHelper.test.js +332 -0
- package/dist/helpers/AIHelper.test.js.map +1 -0
- package/dist/helpers/ConfigHelper.d.ts +20 -0
- package/dist/helpers/ConfigHelper.d.ts.map +1 -0
- package/dist/helpers/ConfigHelper.js +118 -0
- package/dist/helpers/ConfigHelper.js.map +1 -0
- package/dist/helpers/ContextLimiter.d.ts +82 -0
- package/dist/helpers/ContextLimiter.d.ts.map +1 -0
- package/dist/helpers/ContextLimiter.js +165 -0
- package/dist/helpers/ContextLimiter.js.map +1 -0
- package/dist/helpers/FileHelper.d.ts +31 -0
- package/dist/helpers/FileHelper.d.ts.map +1 -0
- package/dist/helpers/FileHelper.js +175 -0
- package/dist/helpers/FileHelper.js.map +1 -0
- package/dist/helpers/SetupHelper.d.ts +5 -0
- package/dist/helpers/SetupHelper.d.ts.map +1 -0
- package/dist/helpers/SetupHelper.js +32 -0
- package/dist/helpers/SetupHelper.js.map +1 -0
- package/dist/helpers/index.d.ts +6 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +6 -0
- package/dist/helpers/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/libs/index.d.ts +4 -0
- package/dist/libs/index.d.ts.map +1 -0
- package/dist/libs/index.js +4 -0
- package/dist/libs/index.js.map +1 -0
- package/dist/libs/kafka/config.d.ts +5 -0
- package/dist/libs/kafka/config.d.ts.map +1 -0
- package/dist/libs/kafka/config.js +5 -0
- package/dist/libs/kafka/config.js.map +1 -0
- package/dist/libs/kafka/consumer.d.ts +16 -0
- package/dist/libs/kafka/consumer.d.ts.map +1 -0
- package/dist/libs/kafka/consumer.js +126 -0
- package/dist/libs/kafka/consumer.js.map +1 -0
- package/dist/libs/kafka/index.d.ts +3 -0
- package/dist/libs/kafka/index.d.ts.map +1 -0
- package/dist/libs/kafka/index.js +3 -0
- package/dist/libs/kafka/index.js.map +1 -0
- package/dist/libs/kafka/kafka.d.ts +3 -0
- package/dist/libs/kafka/kafka.d.ts.map +1 -0
- package/dist/libs/kafka/kafka.js +24 -0
- package/dist/libs/kafka/kafka.js.map +1 -0
- package/dist/libs/kafka/producer.d.ts +11 -0
- package/dist/libs/kafka/producer.d.ts.map +1 -0
- package/dist/libs/kafka/producer.js +44 -0
- package/dist/libs/kafka/producer.js.map +1 -0
- package/dist/libs/logger/config.d.ts +5 -0
- package/dist/libs/logger/config.d.ts.map +1 -0
- package/dist/libs/logger/config.js +6 -0
- package/dist/libs/logger/config.js.map +1 -0
- package/dist/libs/logger/index.d.ts +10 -0
- package/dist/libs/logger/index.d.ts.map +1 -0
- package/dist/libs/logger/index.js +20 -0
- package/dist/libs/logger/index.js.map +1 -0
- package/dist/libs/logger/kafkajs-logger-creator.d.ts +12 -0
- package/dist/libs/logger/kafkajs-logger-creator.d.ts.map +1 -0
- package/dist/libs/logger/kafkajs-logger-creator.js +29 -0
- package/dist/libs/logger/kafkajs-logger-creator.js.map +1 -0
- package/dist/libs/logger/logger.d.ts +42 -0
- package/dist/libs/logger/logger.d.ts.map +1 -0
- package/dist/libs/logger/logger.js +44 -0
- package/dist/libs/logger/logger.js.map +1 -0
- package/dist/libs/s3/config.d.ts +7 -0
- package/dist/libs/s3/config.d.ts.map +1 -0
- package/dist/libs/s3/config.js +7 -0
- package/dist/libs/s3/config.js.map +1 -0
- package/dist/libs/s3/index.d.ts +4 -0
- package/dist/libs/s3/index.d.ts.map +1 -0
- package/dist/libs/s3/index.js +4 -0
- package/dist/libs/s3/index.js.map +1 -0
- package/dist/libs/s3/s3.lib.d.ts +25 -0
- package/dist/libs/s3/s3.lib.d.ts.map +1 -0
- package/dist/libs/s3/s3.lib.js +202 -0
- package/dist/libs/s3/s3.lib.js.map +1 -0
- package/dist/processors/ChatProcessor.d.ts +66 -0
- package/dist/processors/ChatProcessor.d.ts.map +1 -0
- package/dist/processors/ChatProcessor.js +610 -0
- package/dist/processors/ChatProcessor.js.map +1 -0
- package/dist/processors/ModelsProcessor.d.ts +11 -0
- package/dist/processors/ModelsProcessor.d.ts.map +1 -0
- package/dist/processors/ModelsProcessor.js +30 -0
- package/dist/processors/ModelsProcessor.js.map +1 -0
- package/dist/processors/index.d.ts +3 -0
- package/dist/processors/index.d.ts.map +1 -0
- package/dist/processors/index.js +3 -0
- package/dist/processors/index.js.map +1 -0
- package/dist/services/AIService.d.ts +48 -0
- package/dist/services/AIService.d.ts.map +1 -0
- package/dist/services/AIService.js +196 -0
- package/dist/services/AIService.js.map +1 -0
- package/dist/services/InternalEventsHandler.d.ts +21 -0
- package/dist/services/InternalEventsHandler.d.ts.map +1 -0
- package/dist/services/InternalEventsHandler.js +56 -0
- package/dist/services/InternalEventsHandler.js.map +1 -0
- package/dist/services/KafkaService.d.ts +35 -0
- package/dist/services/KafkaService.d.ts.map +1 -0
- package/dist/services/KafkaService.js +120 -0
- package/dist/services/KafkaService.js.map +1 -0
- package/dist/services/ModelFetcher.d.ts +54 -0
- package/dist/services/ModelFetcher.d.ts.map +1 -0
- package/dist/services/ModelFetcher.js +247 -0
- package/dist/services/ModelFetcher.js.map +1 -0
- package/dist/services/RedisService.d.ts +90 -0
- package/dist/services/RedisService.d.ts.map +1 -0
- package/dist/services/RedisService.js +236 -0
- package/dist/services/RedisService.js.map +1 -0
- package/dist/services/SocketService.d.ts +39 -0
- package/dist/services/SocketService.d.ts.map +1 -0
- package/dist/services/SocketService.js +128 -0
- package/dist/services/SocketService.js.map +1 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +7 -0
- package/dist/services/index.js.map +1 -0
- package/dist/store/AgentStore.d.ts +48 -0
- package/dist/store/AgentStore.d.ts.map +1 -0
- package/dist/store/AgentStore.js +98 -0
- package/dist/store/AgentStore.js.map +1 -0
- package/dist/store/ArtifactStore.d.ts +13 -0
- package/dist/store/ArtifactStore.d.ts.map +1 -0
- package/dist/store/ArtifactStore.js +27 -0
- package/dist/store/ArtifactStore.js.map +1 -0
- package/dist/store/ConfigStore.d.ts +89 -0
- package/dist/store/ConfigStore.d.ts.map +1 -0
- package/dist/store/ConfigStore.js +214 -0
- package/dist/store/ConfigStore.js.map +1 -0
- package/dist/store/ConfigStore.test.d.ts +2 -0
- package/dist/store/ConfigStore.test.d.ts.map +1 -0
- package/dist/store/ConfigStore.test.js +259 -0
- package/dist/store/ConfigStore.test.js.map +1 -0
- package/dist/store/ModelStore.d.ts +44 -0
- package/dist/store/ModelStore.d.ts.map +1 -0
- package/dist/store/ModelStore.js +81 -0
- package/dist/store/ModelStore.js.map +1 -0
- package/dist/store/ModelStore.test.d.ts +2 -0
- package/dist/store/ModelStore.test.d.ts.map +1 -0
- package/dist/store/ModelStore.test.js +390 -0
- package/dist/store/ModelStore.test.js.map +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +5 -0
- package/dist/store/index.js.map +1 -0
- package/dist/tools/generateChartTool.d.ts +24 -0
- package/dist/tools/generateChartTool.d.ts.map +1 -0
- package/dist/tools/generateChartTool.js +124 -0
- package/dist/tools/generateChartTool.js.map +1 -0
- package/dist/tools/proposeFormValuesTool.d.ts +35 -0
- package/dist/tools/proposeFormValuesTool.d.ts.map +1 -0
- package/dist/tools/proposeFormValuesTool.js +56 -0
- package/dist/tools/proposeFormValuesTool.js.map +1 -0
- package/package.json +71 -0
- package/src/config.ts +46 -0
- package/src/helpers/AIHelper.test.ts +375 -0
- package/src/helpers/AIHelper.ts +353 -0
- package/src/helpers/ConfigHelper.ts +130 -0
- package/src/helpers/ContextLimiter.ts +228 -0
- package/src/helpers/FileHelper.ts +197 -0
- package/src/helpers/SetupHelper.ts +35 -0
- package/src/helpers/index.ts +5 -0
- package/src/index.ts +18 -0
- package/src/libs/index.ts +3 -0
- package/src/libs/kafka/config.ts +4 -0
- package/src/libs/kafka/consumer.ts +161 -0
- package/src/libs/kafka/index.ts +2 -0
- package/src/libs/kafka/kafka.ts +27 -0
- package/src/libs/kafka/producer.ts +48 -0
- package/src/libs/logger/config.ts +4 -0
- package/src/libs/logger/index.ts +21 -0
- package/src/libs/logger/kafkajs-logger-creator.ts +28 -0
- package/src/libs/logger/logger.ts +60 -0
- package/src/libs/s3/config.ts +7 -0
- package/src/libs/s3/index.ts +3 -0
- package/src/libs/s3/s3.lib.ts +284 -0
- package/src/processors/ChatProcessor.ts +713 -0
- package/src/processors/ModelsProcessor.ts +34 -0
- package/src/processors/index.ts +2 -0
- package/src/services/AIService.ts +241 -0
- package/src/services/InternalEventsHandler.ts +61 -0
- package/src/services/KafkaService.ts +142 -0
- package/src/services/ModelFetcher.ts +286 -0
- package/src/services/RedisService.ts +285 -0
- package/src/services/SocketService.ts +153 -0
- package/src/services/index.ts +6 -0
- package/src/store/AgentStore.ts +138 -0
- package/src/store/ArtifactStore.ts +29 -0
- package/src/store/ConfigStore.test.ts +314 -0
- package/src/store/ConfigStore.ts +239 -0
- package/src/store/ModelStore.test.ts +473 -0
- package/src/store/ModelStore.ts +93 -0
- package/src/store/index.ts +4 -0
- package/src/tools/generateChartTool.ts +131 -0
- package/src/tools/proposeFormValuesTool.ts +67 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { ModelStore } from './ModelStore';
|
|
3
|
+
import type { ModelOption } from '@multiplayer-app/ai-agent-types';
|
|
4
|
+
|
|
5
|
+
describe('ModelStore', () => {
|
|
6
|
+
let modelStore: ModelStore;
|
|
7
|
+
|
|
8
|
+
// Helper to create mock models
|
|
9
|
+
const createMockModel = (
|
|
10
|
+
id: string,
|
|
11
|
+
label: string,
|
|
12
|
+
provider?: string,
|
|
13
|
+
options?: Partial<ModelOption>
|
|
14
|
+
): ModelOption => ({
|
|
15
|
+
id,
|
|
16
|
+
label,
|
|
17
|
+
provider,
|
|
18
|
+
...options
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
// Clear the singleton instance before each test
|
|
23
|
+
// We need to access the private instance property, so we'll clear via clear()
|
|
24
|
+
modelStore = ModelStore.getInstance();
|
|
25
|
+
modelStore.clear();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('getInstance', () => {
|
|
29
|
+
it('should return the same singleton instance on multiple calls', () => {
|
|
30
|
+
const instance1 = ModelStore.getInstance();
|
|
31
|
+
const instance2 = ModelStore.getInstance();
|
|
32
|
+
expect(instance1).toBe(instance2);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should return an instance of ModelStore', () => {
|
|
36
|
+
const instance = ModelStore.getInstance();
|
|
37
|
+
expect(instance).toBeInstanceOf(ModelStore);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('setModels', () => {
|
|
42
|
+
it('should set models in the store', () => {
|
|
43
|
+
const models: ModelOption[] = [
|
|
44
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai'),
|
|
45
|
+
createMockModel('anthropic/claude-3', 'Claude 3', 'anthropic')
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
modelStore.setModels(models);
|
|
49
|
+
expect(modelStore.getModels()).toEqual(models);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should update lastFetched timestamp when setting models', () => {
|
|
53
|
+
const models: ModelOption[] = [
|
|
54
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai')
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const beforeSet = new Date();
|
|
58
|
+
modelStore.setModels(models);
|
|
59
|
+
const afterSet = new Date();
|
|
60
|
+
const lastFetched = modelStore.getLastFetched();
|
|
61
|
+
|
|
62
|
+
expect(lastFetched).not.toBeNull();
|
|
63
|
+
expect(lastFetched!.getTime()).toBeGreaterThanOrEqual(beforeSet.getTime());
|
|
64
|
+
expect(lastFetched!.getTime()).toBeLessThanOrEqual(afterSet.getTime());
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should handle empty array', () => {
|
|
68
|
+
modelStore.setModels([]);
|
|
69
|
+
expect(modelStore.getModels()).toEqual([]);
|
|
70
|
+
expect(modelStore.hasModels()).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should replace existing models when called multiple times', () => {
|
|
74
|
+
const models1: ModelOption[] = [
|
|
75
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai')
|
|
76
|
+
];
|
|
77
|
+
const models2: ModelOption[] = [
|
|
78
|
+
createMockModel('anthropic/claude-3', 'Claude 3', 'anthropic'),
|
|
79
|
+
createMockModel('openai/gpt-3.5', 'GPT-3.5', 'openai')
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
modelStore.setModels(models1);
|
|
83
|
+
expect(modelStore.getModels()).toHaveLength(1);
|
|
84
|
+
|
|
85
|
+
modelStore.setModels(models2);
|
|
86
|
+
expect(modelStore.getModels()).toHaveLength(2);
|
|
87
|
+
expect(modelStore.getModels()).toEqual(models2);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('getModels', () => {
|
|
92
|
+
it('should return empty array when no models are set', () => {
|
|
93
|
+
expect(modelStore.getModels()).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return all models that were set', () => {
|
|
97
|
+
const models: ModelOption[] = [
|
|
98
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai'),
|
|
99
|
+
createMockModel('anthropic/claude-3', 'Claude 3', 'anthropic'),
|
|
100
|
+
createMockModel('google/gemini-pro', 'Gemini Pro', 'google')
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
modelStore.setModels(models);
|
|
104
|
+
expect(modelStore.getModels()).toEqual(models);
|
|
105
|
+
expect(modelStore.getModels()).toHaveLength(3);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return the same array reference (direct reference to internal array)', () => {
|
|
109
|
+
const models: ModelOption[] = [
|
|
110
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai')
|
|
111
|
+
];
|
|
112
|
+
modelStore.setModels(models);
|
|
113
|
+
|
|
114
|
+
const retrieved = modelStore.getModels();
|
|
115
|
+
// Note: getModels() returns the internal array directly, not a copy
|
|
116
|
+
expect(retrieved).toBe(modelStore.getModels()); // Same reference on multiple calls
|
|
117
|
+
expect(retrieved).toEqual(models); // Same content
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('getModelsByProvider', () => {
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
modelStore.setModels([
|
|
124
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai'),
|
|
125
|
+
createMockModel('openai/gpt-3.5', 'GPT-3.5', 'openai'),
|
|
126
|
+
createMockModel('anthropic/claude-3', 'Claude 3', 'anthropic'),
|
|
127
|
+
createMockModel('google/gemini-pro', 'Gemini Pro', 'google')
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should return models for a specific provider', () => {
|
|
132
|
+
const openaiModels = modelStore.getModelsByProvider('openai');
|
|
133
|
+
expect(openaiModels).toHaveLength(2);
|
|
134
|
+
expect(openaiModels.every((m) => m.provider === 'openai')).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should return empty array for non-existent provider', () => {
|
|
138
|
+
const models = modelStore.getModelsByProvider('nonexistent');
|
|
139
|
+
expect(models).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should return empty array for provider with no models', () => {
|
|
143
|
+
const models = modelStore.getModelsByProvider('azure');
|
|
144
|
+
expect(models).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should handle case-sensitive provider names', () => {
|
|
148
|
+
const models = modelStore.getModelsByProvider('OpenAI');
|
|
149
|
+
expect(models).toEqual([]); // Should be case-sensitive
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should return models without provider when filtering for empty string', () => {
|
|
153
|
+
modelStore.setModels([
|
|
154
|
+
createMockModel('model-1', 'Model 1'), // No provider
|
|
155
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai')
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
const modelsWithoutProvider = modelStore.getModelsByProvider('');
|
|
159
|
+
expect(modelsWithoutProvider).toHaveLength(0); // Empty string won't match undefined
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('getModelById', () => {
|
|
164
|
+
beforeEach(() => {
|
|
165
|
+
modelStore.setModels([
|
|
166
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai'),
|
|
167
|
+
createMockModel('anthropic/claude-3-opus', 'Claude 3 Opus', 'anthropic'),
|
|
168
|
+
createMockModel('google/gemini-pro', 'Gemini Pro', 'google'),
|
|
169
|
+
createMockModel('simple-model', 'Simple Model', 'custom')
|
|
170
|
+
]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should return model when exact ID matches', () => {
|
|
174
|
+
const model = modelStore.getModelById('openai/gpt-4');
|
|
175
|
+
expect(model).toBeDefined();
|
|
176
|
+
expect(model?.id).toBe('openai/gpt-4');
|
|
177
|
+
expect(model?.label).toBe('GPT-4');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should return model using fallback logic when exact match not found', () => {
|
|
181
|
+
// split('/', 2) limits to 2 parts, so 'some/prefix/gpt-4' becomes ['some', 'prefix/gpt-4']
|
|
182
|
+
// The fallback looks for models ending with 'prefix/gpt-4', which won't match 'openai/gpt-4'
|
|
183
|
+
// So this test should expect undefined, or we need to test with a 2-part ID
|
|
184
|
+
const model = modelStore.getModelById('some/gpt-4');
|
|
185
|
+
expect(model).toBeDefined();
|
|
186
|
+
expect(model?.id).toBe('openai/gpt-4');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return model using fallback when ID has no provider information', () => {
|
|
190
|
+
const model = modelStore.getModelById('claude-3-opus');
|
|
191
|
+
expect(model).toBeDefined();
|
|
192
|
+
expect(model?.id).toBe('anthropic/claude-3-opus');
|
|
193
|
+
});
|
|
194
|
+
it('should return model when ID has provider info, but model list does not', () => {
|
|
195
|
+
const model = modelStore.getModelById('custom/simple-model');
|
|
196
|
+
expect(model).toBeDefined();
|
|
197
|
+
expect(model?.id).toBe('simple-model');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should return undefined when model not found', () => {
|
|
201
|
+
// Note: empty string ID will match any model because endsWith('') is always true
|
|
202
|
+
// So we test with a clearly non-matching ID
|
|
203
|
+
const model = modelStore.getModelById('nonexistent/definitely-not-a-model');
|
|
204
|
+
expect(model).toBeUndefined();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should return first model for empty string ID (endsWith edge case)', () => {
|
|
208
|
+
// Empty string causes split('/') to return [''], and endsWith('') matches all strings
|
|
209
|
+
// This is a known edge case in the implementation
|
|
210
|
+
const model = modelStore.getModelById('');
|
|
211
|
+
expect(model).toBeDefined(); // Will match first model due to endsWith('') behavior
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should handle ID without slash (no fallback needed)', () => {
|
|
215
|
+
const model = modelStore.getModelById('simple-model');
|
|
216
|
+
expect(model).toBeDefined();
|
|
217
|
+
expect(model?.id).toBe('simple-model');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should handle ID with only one segment (no slash)', () => {
|
|
221
|
+
// When split('/') returns array with one element, idData[idData.length - 1] is that element
|
|
222
|
+
const model = modelStore.getModelById('simple-model');
|
|
223
|
+
expect(model).toBeDefined();
|
|
224
|
+
expect(model?.id).toBe('simple-model');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should prefer exact match over fallback', () => {
|
|
228
|
+
// If we have both exact match and a model that would match fallback
|
|
229
|
+
modelStore.setModels([
|
|
230
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai'),
|
|
231
|
+
createMockModel('custom/gpt-4', 'Custom GPT-4', 'custom')
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const model = modelStore.getModelById('openai/gpt-4');
|
|
235
|
+
expect(model?.id).toBe('openai/gpt-4'); // Should get exact match
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('getProviders', () => {
|
|
240
|
+
it('should return empty array when no models are set', () => {
|
|
241
|
+
expect(modelStore.getProviders()).toEqual([]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should return unique providers from models', () => {
|
|
245
|
+
modelStore.setModels([
|
|
246
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai'),
|
|
247
|
+
createMockModel('openai/gpt-3.5', 'GPT-3.5', 'openai'),
|
|
248
|
+
createMockModel('anthropic/claude-3', 'Claude 3', 'anthropic')
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
const providers = modelStore.getProviders();
|
|
252
|
+
expect(providers).toHaveLength(2);
|
|
253
|
+
expect(providers).toContain('openai');
|
|
254
|
+
expect(providers).toContain('anthropic');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should not include duplicates', () => {
|
|
258
|
+
modelStore.setModels([
|
|
259
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai'),
|
|
260
|
+
createMockModel('openai/gpt-3.5', 'GPT-3.5', 'openai'),
|
|
261
|
+
createMockModel('openai/gpt-4-turbo', 'GPT-4 Turbo', 'openai')
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
const providers = modelStore.getProviders();
|
|
265
|
+
expect(providers).toHaveLength(1);
|
|
266
|
+
expect(providers).toEqual(['openai']);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should exclude models without provider', () => {
|
|
270
|
+
modelStore.setModels([
|
|
271
|
+
createMockModel('model-1', 'Model 1'), // No provider
|
|
272
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai'),
|
|
273
|
+
createMockModel('model-2', 'Model 2') // No provider
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
const providers = modelStore.getProviders();
|
|
277
|
+
expect(providers).toEqual(['openai']);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should return empty array when all models have no provider', () => {
|
|
281
|
+
modelStore.setModels([
|
|
282
|
+
createMockModel('model-1', 'Model 1'),
|
|
283
|
+
createMockModel('model-2', 'Model 2')
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
const providers = modelStore.getProviders();
|
|
287
|
+
expect(providers).toEqual([]);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('getLastFetched', () => {
|
|
292
|
+
it('should return null when no models have been set', () => {
|
|
293
|
+
expect(modelStore.getLastFetched()).toBeNull();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should return date when models have been set', () => {
|
|
297
|
+
const models: ModelOption[] = [
|
|
298
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai')
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
modelStore.setModels(models);
|
|
302
|
+
const lastFetched = modelStore.getLastFetched();
|
|
303
|
+
|
|
304
|
+
expect(lastFetched).not.toBeNull();
|
|
305
|
+
expect(lastFetched).toBeInstanceOf(Date);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should return null after clear()', () => {
|
|
309
|
+
modelStore.setModels([
|
|
310
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai')
|
|
311
|
+
]);
|
|
312
|
+
expect(modelStore.getLastFetched()).not.toBeNull();
|
|
313
|
+
|
|
314
|
+
modelStore.clear();
|
|
315
|
+
expect(modelStore.getLastFetched()).toBeNull();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should update timestamp when setModels is called again', async () => {
|
|
319
|
+
const models: ModelOption[] = [
|
|
320
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai')
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
modelStore.setModels(models);
|
|
324
|
+
const firstTimestamp = modelStore.getLastFetched()!.getTime();
|
|
325
|
+
|
|
326
|
+
// Wait a small amount to ensure different timestamp
|
|
327
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
328
|
+
|
|
329
|
+
modelStore.setModels(models);
|
|
330
|
+
const secondTimestamp = modelStore.getLastFetched()!.getTime();
|
|
331
|
+
|
|
332
|
+
expect(secondTimestamp).toBeGreaterThan(firstTimestamp);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('hasModels', () => {
|
|
337
|
+
it('should return false when no models are set', () => {
|
|
338
|
+
expect(modelStore.hasModels()).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should return true when models are set', () => {
|
|
342
|
+
modelStore.setModels([
|
|
343
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai')
|
|
344
|
+
]);
|
|
345
|
+
expect(modelStore.hasModels()).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should return false after clear()', () => {
|
|
349
|
+
modelStore.setModels([
|
|
350
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai')
|
|
351
|
+
]);
|
|
352
|
+
expect(modelStore.hasModels()).toBe(true);
|
|
353
|
+
|
|
354
|
+
modelStore.clear();
|
|
355
|
+
expect(modelStore.hasModels()).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should return false when empty array is set', () => {
|
|
359
|
+
modelStore.setModels([]);
|
|
360
|
+
expect(modelStore.hasModels()).toBe(false);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe('clear', () => {
|
|
365
|
+
it('should clear all models', () => {
|
|
366
|
+
modelStore.setModels([
|
|
367
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai'),
|
|
368
|
+
createMockModel('anthropic/claude-3', 'Claude 3', 'anthropic')
|
|
369
|
+
]);
|
|
370
|
+
|
|
371
|
+
expect(modelStore.hasModels()).toBe(true);
|
|
372
|
+
modelStore.clear();
|
|
373
|
+
expect(modelStore.hasModels()).toBe(false);
|
|
374
|
+
expect(modelStore.getModels()).toEqual([]);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should reset lastFetched to null', () => {
|
|
378
|
+
modelStore.setModels([
|
|
379
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai')
|
|
380
|
+
]);
|
|
381
|
+
|
|
382
|
+
expect(modelStore.getLastFetched()).not.toBeNull();
|
|
383
|
+
modelStore.clear();
|
|
384
|
+
expect(modelStore.getLastFetched()).toBeNull();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should be safe to call when store is already empty', () => {
|
|
388
|
+
expect(() => modelStore.clear()).not.toThrow();
|
|
389
|
+
expect(modelStore.hasModels()).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should clear providers list', () => {
|
|
393
|
+
modelStore.setModels([
|
|
394
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai')
|
|
395
|
+
]);
|
|
396
|
+
|
|
397
|
+
expect(modelStore.getProviders()).toHaveLength(1);
|
|
398
|
+
modelStore.clear();
|
|
399
|
+
expect(modelStore.getProviders()).toEqual([]);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('integration scenarios', () => {
|
|
404
|
+
it('should handle complete workflow: set, query, clear, set again', () => {
|
|
405
|
+
// Initial state
|
|
406
|
+
expect(modelStore.hasModels()).toBe(false);
|
|
407
|
+
expect(modelStore.getModels()).toEqual([]);
|
|
408
|
+
|
|
409
|
+
// Set models
|
|
410
|
+
const models1: ModelOption[] = [
|
|
411
|
+
createMockModel('openai/gpt-4', 'GPT-4', 'openai'),
|
|
412
|
+
createMockModel('anthropic/claude-3', 'Claude 3', 'anthropic')
|
|
413
|
+
];
|
|
414
|
+
modelStore.setModels(models1);
|
|
415
|
+
|
|
416
|
+
expect(modelStore.hasModels()).toBe(true);
|
|
417
|
+
expect(modelStore.getModels()).toHaveLength(2);
|
|
418
|
+
expect(modelStore.getProviders()).toHaveLength(2);
|
|
419
|
+
expect(modelStore.getModelById('openai/gpt-4')).toBeDefined();
|
|
420
|
+
|
|
421
|
+
// Clear
|
|
422
|
+
modelStore.clear();
|
|
423
|
+
expect(modelStore.hasModels()).toBe(false);
|
|
424
|
+
expect(modelStore.getModels()).toEqual([]);
|
|
425
|
+
expect(modelStore.getProviders()).toEqual([]);
|
|
426
|
+
|
|
427
|
+
// Set again
|
|
428
|
+
const models2: ModelOption[] = [
|
|
429
|
+
createMockModel('google/gemini-pro', 'Gemini Pro', 'google')
|
|
430
|
+
];
|
|
431
|
+
modelStore.setModels(models2);
|
|
432
|
+
|
|
433
|
+
expect(modelStore.hasModels()).toBe(true);
|
|
434
|
+
expect(modelStore.getModels()).toHaveLength(1);
|
|
435
|
+
expect(modelStore.getProviders()).toEqual(['google']);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should handle models with all optional fields', () => {
|
|
439
|
+
const complexModels: ModelOption[] = [
|
|
440
|
+
{
|
|
441
|
+
id: 'openai/gpt-4',
|
|
442
|
+
label: 'GPT-4',
|
|
443
|
+
provider: 'openai',
|
|
444
|
+
reasoning: 'deep',
|
|
445
|
+
contexts: ['context1', 'context2'],
|
|
446
|
+
description: 'A powerful model'
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: 'anthropic/claude-3',
|
|
450
|
+
label: 'Claude 3',
|
|
451
|
+
provider: 'anthropic',
|
|
452
|
+
reasoning: 'concise',
|
|
453
|
+
contexts: ['context1']
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
id: 'simple-model',
|
|
457
|
+
label: 'Simple'
|
|
458
|
+
// No optional fields
|
|
459
|
+
}
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
modelStore.setModels(complexModels);
|
|
463
|
+
|
|
464
|
+
expect(modelStore.getModels()).toHaveLength(3);
|
|
465
|
+
expect(modelStore.getProviders()).toHaveLength(2); // Only 2 have providers
|
|
466
|
+
|
|
467
|
+
const gpt4 = modelStore.getModelById('openai/gpt-4');
|
|
468
|
+
expect(gpt4?.reasoning).toBe('deep');
|
|
469
|
+
expect(gpt4?.contexts).toEqual(['context1', 'context2']);
|
|
470
|
+
expect(gpt4?.description).toBe('A powerful model');
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ModelOption } from '@multiplayer-app/ai-agent-types';
|
|
2
|
+
import { logger } from '../libs/logger';
|
|
3
|
+
import { ConfigStore } from './ConfigStore';
|
|
4
|
+
|
|
5
|
+
export class ModelStore {
|
|
6
|
+
private static instance: ModelStore;
|
|
7
|
+
private models: ModelOption[] = [];
|
|
8
|
+
private lastFetched: Date | null = null;
|
|
9
|
+
|
|
10
|
+
private constructor() {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the singleton instance of ModelStore
|
|
14
|
+
*/
|
|
15
|
+
static getInstance(): ModelStore {
|
|
16
|
+
if (!ModelStore.instance) {
|
|
17
|
+
ModelStore.instance = new ModelStore();
|
|
18
|
+
}
|
|
19
|
+
return ModelStore.instance;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set models in the store
|
|
24
|
+
*/
|
|
25
|
+
setModels(models: ModelOption[]): void {
|
|
26
|
+
this.models = models.filter((model) => ConfigStore.getInstance().isModelAllowed(model.id, model.provider));
|
|
27
|
+
this.lastFetched = new Date();
|
|
28
|
+
logger.info(`Loaded ${models.length} models, available after filtering: ${this.models.length} from ${this.getProviders().length} providers`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get all models
|
|
33
|
+
*/
|
|
34
|
+
getModels(): ModelOption[] {
|
|
35
|
+
return this.models;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get models by provider
|
|
40
|
+
*/
|
|
41
|
+
getModelsByProvider(provider: string): ModelOption[] {
|
|
42
|
+
return this.models.filter((model) => model.provider === provider);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get a specific model by ID
|
|
47
|
+
*/
|
|
48
|
+
getModelById(id: string): ModelOption | undefined {
|
|
49
|
+
const found = this.models.find((model) => model.id === id);
|
|
50
|
+
|
|
51
|
+
if (found) {
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
const idData = id.split('/', 2)
|
|
55
|
+
const modelName = idData[idData.length - 1];
|
|
56
|
+
return this.models.find((model) => model.id.endsWith(modelName));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get all unique providers
|
|
61
|
+
*/
|
|
62
|
+
getProviders(): string[] {
|
|
63
|
+
const providers = new Set<string>();
|
|
64
|
+
for (const model of this.models) {
|
|
65
|
+
if (model.provider) {
|
|
66
|
+
providers.add(model.provider);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return Array.from(providers);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get when models were last fetched
|
|
74
|
+
*/
|
|
75
|
+
getLastFetched(): Date | null {
|
|
76
|
+
return this.lastFetched;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if models have been loaded
|
|
81
|
+
*/
|
|
82
|
+
hasModels(): boolean {
|
|
83
|
+
return this.models.length > 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clear all models
|
|
88
|
+
*/
|
|
89
|
+
clear(): void {
|
|
90
|
+
this.models = [];
|
|
91
|
+
this.lastFetched = null;
|
|
92
|
+
}
|
|
93
|
+
}
|