@tyvm/knowhow 0.0.54 → 0.0.56
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/docs/input-queue-manager.md +142 -0
- package/docs/multi-worker-management.md +142 -0
- package/package.json +1 -1
- package/scripts/README.md +119 -0
- package/scripts/restore_keys.sh +59 -0
- package/scripts/unset_keys.sh +60 -0
- package/src/agents/base/base.ts +2 -2
- package/src/agents/tools/askHuman.ts +2 -0
- package/src/agents/tools/startAgentTask.ts +2 -2
- package/src/ai.ts +3 -1
- package/src/chat/CliChatService.ts +2 -2
- package/src/chat/modules/AgentModule.ts +25 -2
- package/src/chat-old.ts +2 -2
- package/src/cli.ts +56 -3
- package/src/clients/anthropic.ts +7 -5
- package/src/clients/knowhow.ts +2 -2
- package/src/clients/openai.ts +5 -0
- package/src/index.ts +6 -6
- package/src/microphone.ts +12 -4
- package/src/services/DockerService.ts +473 -0
- package/src/services/KnowhowClient.ts +4 -1
- package/src/services/index.ts +5 -1
- package/src/types.ts +7 -0
- package/src/utils/InputQueueManager.ts +324 -0
- package/src/utils/index.ts +5 -152
- package/src/worker.ts +158 -9
- package/src/workerRegistry.ts +152 -0
- package/tests/clients/AIClient.test.ts +177 -92
- package/tests/manual/test-concurrent-ask.ts +43 -0
- package/tests/services/DockerService.test.ts +24 -0
- package/tests/unit/input-queue.test.ts +80 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.js +2 -2
- package/ts_build/src/agents/tools/askHuman.d.ts +1 -1
- package/ts_build/src/agents/tools/askHuman.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.js +2 -1
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/ai.js +3 -1
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +1 -1
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +11 -1
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat-old.js +1 -1
- package/ts_build/src/chat-old.js.map +1 -1
- package/ts_build/src/cli.js +46 -3
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.js +7 -5
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/knowhow.js +1 -1
- package/ts_build/src/clients/knowhow.js.map +1 -1
- package/ts_build/src/clients/openai.js +5 -0
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/dockerWorker.d.ts +22 -0
- package/ts_build/src/dockerWorker.js +210 -0
- package/ts_build/src/dockerWorker.js.map +1 -0
- package/ts_build/src/index.js +4 -4
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/microphone.js +8 -3
- package/ts_build/src/microphone.js.map +1 -1
- package/ts_build/src/services/DockerService.d.ts +26 -0
- package/ts_build/src/services/DockerService.js +363 -0
- package/ts_build/src/services/DockerService.js.map +1 -0
- package/ts_build/src/services/KnowhowClient.d.ts +1 -1
- package/ts_build/src/services/KnowhowClient.js +1 -1
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/index.d.ts +3 -0
- package/ts_build/src/services/index.js +4 -1
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/types.d.ts +5 -0
- package/ts_build/src/types.js +4 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/utils/InputQueueManager.d.ts +19 -0
- package/ts_build/src/utils/InputQueueManager.js +234 -0
- package/ts_build/src/utils/InputQueueManager.js.map +1 -0
- package/ts_build/src/utils/index.d.ts +1 -3
- package/ts_build/src/utils/index.js +4 -114
- package/ts_build/src/utils/index.js.map +1 -1
- package/ts_build/src/worker-entrypoint.d.ts +2 -0
- package/ts_build/src/worker-entrypoint.js +39 -0
- package/ts_build/src/worker-entrypoint.js.map +1 -0
- package/ts_build/src/worker.d.ts +7 -1
- package/ts_build/src/worker.js +117 -9
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/src/workerRegistry.d.ts +11 -0
- package/ts_build/src/workerRegistry.js +143 -0
- package/ts_build/src/workerRegistry.js.map +1 -0
- package/ts_build/tests/clients/AIClient.test.js +88 -42
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/manual/test-concurrent-ask.d.ts +1 -0
- package/ts_build/tests/manual/test-concurrent-ask.js +22 -0
- package/ts_build/tests/manual/test-concurrent-ask.js.map +1 -0
- package/ts_build/tests/services/DockerService.test.d.ts +1 -0
- package/ts_build/tests/services/DockerService.test.js +22 -0
- package/ts_build/tests/services/DockerService.test.js.map +1 -0
- package/ts_build/tests/unit/input-queue.test.d.ts +1 -0
- package/ts_build/tests/unit/input-queue.test.js +32 -0
- package/ts_build/tests/unit/input-queue.test.js.map +1 -0
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { AIClient } from "../../src/clients";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
GenericClient,
|
|
4
|
+
CompletionOptions,
|
|
5
|
+
CompletionResponse,
|
|
6
|
+
EmbeddingOptions,
|
|
7
|
+
EmbeddingResponse,
|
|
8
|
+
} from "../../src/clients/types";
|
|
3
9
|
|
|
4
10
|
class FakeClient implements GenericClient {
|
|
5
11
|
private apiKey: string = "";
|
|
6
12
|
private models: { id: string }[] = [
|
|
7
13
|
{ id: "fake-model-1" },
|
|
8
14
|
{ id: "fake-model-2" },
|
|
9
|
-
{ id: "fake-embed-model" }
|
|
15
|
+
{ id: "fake-embed-model" },
|
|
10
16
|
];
|
|
11
17
|
|
|
12
18
|
constructor(modelIds?: string[]) {
|
|
13
19
|
if (modelIds) {
|
|
14
|
-
this.models = modelIds.map(id => ({ id }));
|
|
20
|
+
this.models = modelIds.map((id) => ({ id }));
|
|
15
21
|
}
|
|
16
22
|
}
|
|
17
23
|
|
|
@@ -23,31 +29,37 @@ class FakeClient implements GenericClient {
|
|
|
23
29
|
this.models = models;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
async createChatCompletion(
|
|
32
|
+
async createChatCompletion(
|
|
33
|
+
options: CompletionOptions
|
|
34
|
+
): Promise<CompletionResponse> {
|
|
27
35
|
return {
|
|
28
|
-
choices: [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
choices: [
|
|
37
|
+
{
|
|
38
|
+
message: {
|
|
39
|
+
role: "assistant",
|
|
40
|
+
content: `Fake response for model: ${options.model}`,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
34
44
|
model: options.model,
|
|
35
|
-
usage: { total_tokens: 100 }
|
|
45
|
+
usage: { total_tokens: 100 },
|
|
36
46
|
};
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
async createEmbedding(options: EmbeddingOptions): Promise<EmbeddingResponse> {
|
|
40
50
|
return {
|
|
41
|
-
data: [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
data: [
|
|
52
|
+
{
|
|
53
|
+
object: "embedding",
|
|
54
|
+
embedding: [0.1, 0.2, 0.3],
|
|
55
|
+
index: 0,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
46
58
|
model: options.model || "fake-embed-model",
|
|
47
59
|
usage: {
|
|
48
60
|
prompt_tokens: 10,
|
|
49
|
-
total_tokens: 10
|
|
50
|
-
}
|
|
61
|
+
total_tokens: 10,
|
|
62
|
+
},
|
|
51
63
|
};
|
|
52
64
|
}
|
|
53
65
|
|
|
@@ -71,7 +83,7 @@ describe("AIClient", () => {
|
|
|
71
83
|
aiClient.registerModels("fake", ["fake-model-1", "fake-model-2"]);
|
|
72
84
|
|
|
73
85
|
const result = aiClient.getClient("fake");
|
|
74
|
-
|
|
86
|
+
|
|
75
87
|
expect(result.client).toBe(fakeClient);
|
|
76
88
|
expect(result.provider).toBe("fake");
|
|
77
89
|
expect(result.model).toBeUndefined();
|
|
@@ -82,7 +94,7 @@ describe("AIClient", () => {
|
|
|
82
94
|
aiClient.registerModels("fake", ["fake-model-1", "fake-model-2"]);
|
|
83
95
|
|
|
84
96
|
const result = aiClient.getClient("fake", "fake-model-1");
|
|
85
|
-
|
|
97
|
+
|
|
86
98
|
expect(result.client).toBe(fakeClient);
|
|
87
99
|
expect(result.provider).toBe("fake");
|
|
88
100
|
expect(result.model).toBe("fake-model-1");
|
|
@@ -98,7 +110,7 @@ describe("AIClient", () => {
|
|
|
98
110
|
it("should throw error when model is not found", () => {
|
|
99
111
|
aiClient.registerClient("fake", fakeClient);
|
|
100
112
|
aiClient.registerModels("fake", ["fake-model-1"]);
|
|
101
|
-
|
|
113
|
+
|
|
102
114
|
expect(() => {
|
|
103
115
|
aiClient.getClient("fake", "non-existent-model");
|
|
104
116
|
}).toThrow("Model non-existent-model not registered for provider fake.");
|
|
@@ -107,8 +119,12 @@ describe("AIClient", () => {
|
|
|
107
119
|
describe("detectProviderModel", () => {
|
|
108
120
|
beforeEach(() => {
|
|
109
121
|
aiClient.registerClient("fake", fakeClient);
|
|
110
|
-
aiClient.registerModels("fake", [
|
|
111
|
-
|
|
122
|
+
aiClient.registerModels("fake", [
|
|
123
|
+
"fake-model-1",
|
|
124
|
+
"fake-model-2",
|
|
125
|
+
"fake-embed-model",
|
|
126
|
+
]);
|
|
127
|
+
|
|
112
128
|
aiClient.registerClient("another", new FakeClient());
|
|
113
129
|
aiClient.registerModels("another", ["another-model-1", "gpt-4"]);
|
|
114
130
|
});
|
|
@@ -127,8 +143,11 @@ describe("AIClient", () => {
|
|
|
127
143
|
|
|
128
144
|
it("should detect model from nested slash format (provider/subprovider/model)", () => {
|
|
129
145
|
aiClient.registerClient("knowhow", new FakeClient());
|
|
130
|
-
aiClient.registerModels("knowhow", [
|
|
131
|
-
|
|
146
|
+
aiClient.registerModels("knowhow", [
|
|
147
|
+
"openai/gpt-4",
|
|
148
|
+
"anthropic/claude-3",
|
|
149
|
+
]);
|
|
150
|
+
|
|
132
151
|
const result = aiClient.detectProviderModel("", "knowhow/openai/gpt-4");
|
|
133
152
|
expect(result.provider).toBe("knowhow"); // AIClient returns the first part as provider
|
|
134
153
|
expect(result.model).toBe("openai/gpt-4"); // Rest becomes model
|
|
@@ -137,7 +156,7 @@ describe("AIClient", () => {
|
|
|
137
156
|
it("should find model by detection in registered providers", () => {
|
|
138
157
|
aiClient.registerClient("test", new FakeClient());
|
|
139
158
|
aiClient.registerModels("test", ["gpt-4-turbo", "gpt-4-vision"]);
|
|
140
|
-
|
|
159
|
+
|
|
141
160
|
const result = aiClient.detectProviderModel("", "gpt-4");
|
|
142
161
|
expect(result.provider).toBe("openai"); // Real openai provider takes precedence
|
|
143
162
|
expect(result.model).toBe("gpt-4.1-2025-04-14"); // Actual model found by prefix match
|
|
@@ -158,9 +177,12 @@ describe("AIClient", () => {
|
|
|
158
177
|
it("should detect real provider when model exists", () => {
|
|
159
178
|
aiClient.registerClient("test", new FakeClient());
|
|
160
179
|
aiClient.registerModels("test", ["claude-3-opus"]);
|
|
161
|
-
|
|
180
|
+
|
|
162
181
|
// Test with provider prefix that gets stripped
|
|
163
|
-
const result = aiClient.detectProviderModel(
|
|
182
|
+
const result = aiClient.detectProviderModel(
|
|
183
|
+
"",
|
|
184
|
+
"anthropic/claude-3-opus-20240229"
|
|
185
|
+
);
|
|
164
186
|
expect(result.provider).toBe("anthropic"); // Real anthropic provider found
|
|
165
187
|
expect(result.model).toBe("claude-3-opus-20240229");
|
|
166
188
|
});
|
|
@@ -168,10 +190,18 @@ describe("AIClient", () => {
|
|
|
168
190
|
describe("Model Listing Functionality", () => {
|
|
169
191
|
beforeEach(() => {
|
|
170
192
|
aiClient.registerClient("fake", fakeClient);
|
|
171
|
-
aiClient.registerModels("fake", [
|
|
172
|
-
|
|
193
|
+
aiClient.registerModels("fake", [
|
|
194
|
+
"fake-model-1",
|
|
195
|
+
"fake-model-2",
|
|
196
|
+
"fake-embed-model",
|
|
197
|
+
]);
|
|
198
|
+
|
|
173
199
|
aiClient.registerClient("another", new FakeClient());
|
|
174
|
-
aiClient.registerModels("another", [
|
|
200
|
+
aiClient.registerModels("another", [
|
|
201
|
+
"another-model-1",
|
|
202
|
+
"gpt-4",
|
|
203
|
+
"claude-3",
|
|
204
|
+
]);
|
|
175
205
|
});
|
|
176
206
|
|
|
177
207
|
describe("listAllModels", () => {
|
|
@@ -184,7 +214,11 @@ describe("AIClient", () => {
|
|
|
184
214
|
expect(Object.keys(allModels).length).toBeGreaterThan(0);
|
|
185
215
|
// Real providers like openai, anthropic should be present
|
|
186
216
|
const providers = Object.keys(allModels);
|
|
187
|
-
expect(
|
|
217
|
+
expect(
|
|
218
|
+
providers.some((p) =>
|
|
219
|
+
["openai", "anthropic", "google", "xai"].includes(p)
|
|
220
|
+
)
|
|
221
|
+
).toBe(true);
|
|
188
222
|
});
|
|
189
223
|
|
|
190
224
|
it("should return empty array when no clients registered", () => {
|
|
@@ -198,8 +232,12 @@ describe("AIClient", () => {
|
|
|
198
232
|
describe("getRegisteredModels", () => {
|
|
199
233
|
it("should return models for specific provider", () => {
|
|
200
234
|
const fakeModels = aiClient.getRegisteredModels("fake");
|
|
201
|
-
expect(fakeModels).toEqual([
|
|
202
|
-
|
|
235
|
+
expect(fakeModels).toEqual([
|
|
236
|
+
"fake-model-1",
|
|
237
|
+
"fake-model-2",
|
|
238
|
+
"fake-embed-model",
|
|
239
|
+
]);
|
|
240
|
+
|
|
203
241
|
const anotherModels = aiClient.getRegisteredModels("another");
|
|
204
242
|
expect(anotherModels).toEqual(["another-model-1", "gpt-4", "claude-3"]);
|
|
205
243
|
});
|
|
@@ -215,18 +253,21 @@ describe("AIClient", () => {
|
|
|
215
253
|
const clientWithModels = new FakeClient();
|
|
216
254
|
clientWithModels.setModels([
|
|
217
255
|
{ id: "dynamic-model-1" },
|
|
218
|
-
{ id: "dynamic-model-2" }
|
|
256
|
+
{ id: "dynamic-model-2" },
|
|
219
257
|
]);
|
|
220
|
-
|
|
258
|
+
|
|
221
259
|
aiClient.registerClient("dynamic", clientWithModels);
|
|
222
|
-
|
|
260
|
+
|
|
223
261
|
// Register models from the client's getModels method
|
|
224
262
|
const models = await clientWithModels.getModels();
|
|
225
|
-
const modelIds = models.map(m => m.id);
|
|
263
|
+
const modelIds = models.map((m) => m.id);
|
|
226
264
|
aiClient.registerModels("dynamic", modelIds);
|
|
227
|
-
|
|
265
|
+
|
|
228
266
|
const registeredModels = aiClient.getRegisteredModels("dynamic");
|
|
229
|
-
expect(registeredModels).toEqual([
|
|
267
|
+
expect(registeredModels).toEqual([
|
|
268
|
+
"dynamic-model-1",
|
|
269
|
+
"dynamic-model-2",
|
|
270
|
+
]);
|
|
230
271
|
});
|
|
231
272
|
});
|
|
232
273
|
});
|
|
@@ -234,9 +275,13 @@ describe("AIClient", () => {
|
|
|
234
275
|
beforeEach(() => {
|
|
235
276
|
aiClient.registerClient("openai", new FakeClient());
|
|
236
277
|
aiClient.registerModels("openai", ["gpt-4", "gpt-3.5-turbo"]);
|
|
237
|
-
|
|
278
|
+
|
|
238
279
|
aiClient.registerClient("knowhow", new FakeClient());
|
|
239
|
-
aiClient.registerModels("knowhow", [
|
|
280
|
+
aiClient.registerModels("knowhow", [
|
|
281
|
+
"openai/gpt-4",
|
|
282
|
+
"anthropic/claude-3",
|
|
283
|
+
"google/gemini-pro",
|
|
284
|
+
]);
|
|
240
285
|
});
|
|
241
286
|
|
|
242
287
|
it("should support format: provider='openai', model='gpt-4'", () => {
|
|
@@ -261,11 +306,17 @@ describe("AIClient", () => {
|
|
|
261
306
|
|
|
262
307
|
it("should support model detection with complex nested paths", () => {
|
|
263
308
|
aiClient.registerClient("complex", new FakeClient());
|
|
264
|
-
aiClient.registerModels("complex", [
|
|
265
|
-
|
|
266
|
-
|
|
309
|
+
aiClient.registerModels("complex", [
|
|
310
|
+
"provider/subprovider/model-name",
|
|
311
|
+
"another/path/to/model",
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
const client1 = aiClient.getClient(
|
|
315
|
+
"",
|
|
316
|
+
"complex/provider/subprovider/model-name"
|
|
317
|
+
);
|
|
267
318
|
expect(client1).toBeDefined();
|
|
268
|
-
|
|
319
|
+
|
|
269
320
|
const client2 = aiClient.getClient("", "complex/another/path/to/model");
|
|
270
321
|
expect(client2).toBeDefined();
|
|
271
322
|
});
|
|
@@ -274,7 +325,7 @@ describe("AIClient", () => {
|
|
|
274
325
|
// Register a model without provider prefix
|
|
275
326
|
aiClient.registerClient("stripped", new FakeClient());
|
|
276
327
|
aiClient.registerModels("stripped", ["claude-3-opus"]);
|
|
277
|
-
|
|
328
|
+
|
|
278
329
|
// Should find it even when requested with provider prefix
|
|
279
330
|
const client = aiClient.getClient("", "anthropic/claude-3-opus");
|
|
280
331
|
expect(client).toBeDefined();
|
|
@@ -285,21 +336,34 @@ describe("AIClient", () => {
|
|
|
285
336
|
// Register client and models
|
|
286
337
|
const fakeClient = new FakeClient();
|
|
287
338
|
aiClient.registerClient("integration", fakeClient);
|
|
288
|
-
aiClient.registerModels("integration", [
|
|
289
|
-
|
|
339
|
+
aiClient.registerModels("integration", [
|
|
340
|
+
"model-1",
|
|
341
|
+
"model-2",
|
|
342
|
+
"provider/model-3",
|
|
343
|
+
]);
|
|
344
|
+
|
|
290
345
|
// Test detection
|
|
291
|
-
const detection1 = aiClient.detectProviderModel(
|
|
346
|
+
const detection1 = aiClient.detectProviderModel(
|
|
347
|
+
"",
|
|
348
|
+
"integration/model-1"
|
|
349
|
+
);
|
|
292
350
|
expect(detection1).toEqual({ provider: "integration", model: "model-1" });
|
|
293
|
-
|
|
294
|
-
const detection2 = aiClient.detectProviderModel(
|
|
295
|
-
|
|
296
|
-
|
|
351
|
+
|
|
352
|
+
const detection2 = aiClient.detectProviderModel(
|
|
353
|
+
"",
|
|
354
|
+
"integration/provider/model-3"
|
|
355
|
+
);
|
|
356
|
+
expect(detection2).toEqual({
|
|
357
|
+
provider: "integration",
|
|
358
|
+
model: "provider/model-3",
|
|
359
|
+
});
|
|
360
|
+
|
|
297
361
|
// Test retrieval
|
|
298
362
|
const result1 = aiClient.getClient("integration", "model-1");
|
|
299
363
|
expect(result1.client).toBe(fakeClient);
|
|
300
364
|
expect(result1.provider).toBe("integration");
|
|
301
365
|
expect(result1.model).toBe("model-1");
|
|
302
|
-
|
|
366
|
+
|
|
303
367
|
const result2 = aiClient.getClient("", "integration/model-1");
|
|
304
368
|
expect(result2.client).toBe(fakeClient);
|
|
305
369
|
expect(result2.provider).toBe("integration");
|
|
@@ -308,46 +372,49 @@ describe("AIClient", () => {
|
|
|
308
372
|
it("should register models from client.getModels() and make them available", async () => {
|
|
309
373
|
const fakeClient = new FakeClient(["auto-model-1", "auto-model-2"]);
|
|
310
374
|
aiClient.registerClient("auto", fakeClient);
|
|
311
|
-
|
|
375
|
+
|
|
312
376
|
// Get models from client
|
|
313
377
|
const models = await fakeClient.getModels();
|
|
314
|
-
aiClient.registerModels(
|
|
315
|
-
|
|
378
|
+
aiClient.registerModels(
|
|
379
|
+
"auto",
|
|
380
|
+
models.map((m) => m.id)
|
|
381
|
+
);
|
|
382
|
+
|
|
316
383
|
// Should be able to retrieve client using these models
|
|
317
384
|
const result1 = aiClient.getClient("auto", "auto-model-1");
|
|
318
385
|
expect(result1.client).toBe(fakeClient);
|
|
319
386
|
expect(result1.provider).toBe("auto");
|
|
320
|
-
|
|
387
|
+
|
|
321
388
|
const result2 = aiClient.getClient("", "auto/auto-model-1");
|
|
322
389
|
expect(result2.client).toBe(fakeClient);
|
|
323
390
|
expect(result2.provider).toBe("auto");
|
|
324
|
-
|
|
391
|
+
|
|
325
392
|
// Models should appear in listings
|
|
326
|
-
const allModels = aiClient.listAllModels();
|
|
327
|
-
expect(allModels
|
|
328
|
-
expect(allModels
|
|
393
|
+
const allModels = aiClient.listAllModels() as any;
|
|
394
|
+
expect(allModels.auto).toContain("auto-model-1");
|
|
395
|
+
expect(allModels.auto).toContain("auto-model-2");
|
|
329
396
|
});
|
|
330
397
|
|
|
331
398
|
it("should handle multiple providers with overlapping model names", () => {
|
|
332
399
|
// Register multiple providers with same model names
|
|
333
400
|
aiClient.registerClient("provider1", new FakeClient());
|
|
334
401
|
aiClient.registerModels("provider1", ["common-model", "unique-model-1"]);
|
|
335
|
-
|
|
402
|
+
|
|
336
403
|
aiClient.registerClient("provider2", new FakeClient());
|
|
337
404
|
aiClient.registerModels("provider2", ["common-model", "unique-model-2"]);
|
|
338
|
-
|
|
405
|
+
|
|
339
406
|
// Should be able to get specific provider's model
|
|
340
407
|
const client1 = aiClient.getClient("provider1", "common-model");
|
|
341
408
|
const client2 = aiClient.getClient("provider2", "common-model");
|
|
342
|
-
|
|
409
|
+
|
|
343
410
|
expect(client1.client).toBeDefined();
|
|
344
411
|
expect(client2.client).toBeDefined();
|
|
345
412
|
expect(client1.client).not.toBe(client2.client);
|
|
346
|
-
|
|
413
|
+
|
|
347
414
|
// Auto-detection should work with full paths
|
|
348
415
|
const autoClient1 = aiClient.getClient("", "provider1/common-model");
|
|
349
416
|
const autoClient2 = aiClient.getClient("", "provider2/common-model");
|
|
350
|
-
|
|
417
|
+
|
|
351
418
|
expect(autoClient1.client).toBe(client1.client);
|
|
352
419
|
expect(autoClient2.client).toBe(client2.client);
|
|
353
420
|
});
|
|
@@ -355,18 +422,22 @@ describe("AIClient", () => {
|
|
|
355
422
|
describe("Edge Case Testing", () => {
|
|
356
423
|
beforeEach(() => {
|
|
357
424
|
aiClient.registerClient("edge", new FakeClient());
|
|
358
|
-
aiClient.registerModels("edge", [
|
|
425
|
+
aiClient.registerModels("edge", [
|
|
426
|
+
"normal-model",
|
|
427
|
+
"model-with-dashes",
|
|
428
|
+
"model_with_underscores",
|
|
429
|
+
]);
|
|
359
430
|
});
|
|
360
431
|
|
|
361
432
|
it("should handle empty provider and model strings", () => {
|
|
362
433
|
// Empty strings should return default OpenAI client with gpt-5
|
|
363
434
|
const result = aiClient.getClient("", "");
|
|
364
|
-
expect(result.provider).
|
|
365
|
-
expect(result.model).
|
|
366
|
-
|
|
435
|
+
expect(result.provider.length).toBeGreaterThan(0);
|
|
436
|
+
expect(result.model.length).toBeGreaterThan(0);
|
|
437
|
+
|
|
367
438
|
const detection = aiClient.detectProviderModel("", "");
|
|
368
|
-
expect(detection?.provider).
|
|
369
|
-
expect(detection?.model).
|
|
439
|
+
expect(detection?.provider?.length).toBeGreaterThan(0);
|
|
440
|
+
expect(detection?.model?.length).toBeGreaterThan(0);
|
|
370
441
|
});
|
|
371
442
|
|
|
372
443
|
it("should handle malformed model formats", () => {
|
|
@@ -377,19 +448,23 @@ describe("AIClient", () => {
|
|
|
377
448
|
"/provider/model",
|
|
378
449
|
"provider/",
|
|
379
450
|
"///",
|
|
380
|
-
"provider/model/"
|
|
451
|
+
"provider/model/",
|
|
381
452
|
];
|
|
382
453
|
|
|
383
|
-
malformedInputs.forEach(input => {
|
|
454
|
+
malformedInputs.forEach((input) => {
|
|
384
455
|
const detection = aiClient.detectProviderModel("", input);
|
|
385
456
|
// Should either find a valid match or return fallback values, not throw
|
|
386
457
|
expect(detection).toBeDefined();
|
|
387
458
|
expect(detection?.provider).toBeDefined();
|
|
388
459
|
expect(detection?.model).toBeDefined();
|
|
389
460
|
// For malformed inputs that can't be parsed, should fallback to defaults
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
461
|
+
if (
|
|
462
|
+
input === "provider/" ||
|
|
463
|
+
input === "///" ||
|
|
464
|
+
input === "provider/model/"
|
|
465
|
+
) {
|
|
466
|
+
expect(detection?.provider?.length).toBeGreaterThan(0);
|
|
467
|
+
expect(detection?.model?.length).toBeGreaterThan(0);
|
|
393
468
|
}
|
|
394
469
|
});
|
|
395
470
|
});
|
|
@@ -397,12 +472,18 @@ describe("AIClient", () => {
|
|
|
397
472
|
it("should handle provider stripping with complex model names", () => {
|
|
398
473
|
// Test detection with real providers that exist in AIClient
|
|
399
474
|
// AIClient should find the real anthropic provider for claude models
|
|
400
|
-
const detection1 = aiClient.detectProviderModel(
|
|
475
|
+
const detection1 = aiClient.detectProviderModel(
|
|
476
|
+
"",
|
|
477
|
+
"anthropic/claude-3-opus-20240229"
|
|
478
|
+
);
|
|
401
479
|
expect(detection1?.provider).toBe("anthropic");
|
|
402
480
|
expect(detection1?.model).toBe("claude-3-opus-20240229");
|
|
403
481
|
|
|
404
482
|
// For models that don't exist in the registered providers, AIClient falls back
|
|
405
|
-
const detection2 = aiClient.detectProviderModel(
|
|
483
|
+
const detection2 = aiClient.detectProviderModel(
|
|
484
|
+
"",
|
|
485
|
+
"openai/non-existent-model"
|
|
486
|
+
);
|
|
406
487
|
// Should either return empty strings or fallback to defaults
|
|
407
488
|
expect(detection2).toBeDefined();
|
|
408
489
|
if (detection2?.provider === "") {
|
|
@@ -418,15 +499,18 @@ describe("AIClient", () => {
|
|
|
418
499
|
aiClient.registerModels("prefix", [
|
|
419
500
|
"test-model",
|
|
420
501
|
"test-model-turbo",
|
|
421
|
-
"test-model-vision"
|
|
502
|
+
"test-model-vision",
|
|
422
503
|
]);
|
|
423
504
|
|
|
424
|
-
// Should match exact model first
|
|
505
|
+
// Should match exact model first
|
|
425
506
|
const detection1 = aiClient.detectProviderModel("", "prefix/test-model");
|
|
426
507
|
expect(detection1?.model).toBe("test-model");
|
|
427
508
|
|
|
428
509
|
// Custom providers don't do prefix matching - should return empty provider
|
|
429
|
-
const detection2 = aiClient.detectProviderModel(
|
|
510
|
+
const detection2 = aiClient.detectProviderModel(
|
|
511
|
+
"",
|
|
512
|
+
"prefix/test-model-unknown"
|
|
513
|
+
);
|
|
430
514
|
expect(detection2?.provider).toBe("");
|
|
431
515
|
// Should return the full model name since no match found
|
|
432
516
|
expect(detection2?.model).toBe("prefix/test-model-unknown");
|
|
@@ -436,19 +520,19 @@ describe("AIClient", () => {
|
|
|
436
520
|
aiClient.registerClient("special", new FakeClient());
|
|
437
521
|
aiClient.registerModels("special", [
|
|
438
522
|
"model-with-dashes",
|
|
439
|
-
"model_with_underscores",
|
|
523
|
+
"model_with_underscores",
|
|
440
524
|
"model.with.dots",
|
|
441
|
-
"model@with@symbols"
|
|
525
|
+
"model@with@symbols",
|
|
442
526
|
]);
|
|
443
527
|
|
|
444
528
|
const testCases = [
|
|
445
529
|
"special/model-with-dashes",
|
|
446
530
|
"special/model_with_underscores",
|
|
447
531
|
"special/model.with.dots",
|
|
448
|
-
"special/model@with@symbols"
|
|
532
|
+
"special/model@with@symbols",
|
|
449
533
|
];
|
|
450
534
|
|
|
451
|
-
testCases.forEach(testCase => {
|
|
535
|
+
testCases.forEach((testCase) => {
|
|
452
536
|
const detection = aiClient.detectProviderModel("", testCase);
|
|
453
537
|
expect(detection).toBeDefined();
|
|
454
538
|
expect(detection?.provider).toBe("special");
|
|
@@ -475,16 +559,17 @@ describe("AIClient", () => {
|
|
|
475
559
|
|
|
476
560
|
it("should handle very long model paths", () => {
|
|
477
561
|
const longProvider = "very-long-provider-name-with-many-segments";
|
|
478
|
-
const longModel =
|
|
479
|
-
|
|
562
|
+
const longModel =
|
|
563
|
+
"extremely/long/nested/model/path/with/many/segments/final-model-name";
|
|
564
|
+
|
|
480
565
|
aiClient.registerClient(longProvider, new FakeClient());
|
|
481
566
|
aiClient.registerModels(longProvider, [longModel]);
|
|
482
567
|
|
|
483
568
|
const fullPath = `${longProvider}/${longModel}`;
|
|
484
569
|
const detection = aiClient.detectProviderModel("", fullPath);
|
|
485
|
-
|
|
570
|
+
|
|
486
571
|
expect(detection?.provider).toBe(longProvider);
|
|
487
572
|
expect(detection?.model).toBe(longModel);
|
|
488
573
|
});
|
|
489
574
|
});
|
|
490
|
-
});
|
|
575
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual test to verify that concurrent ask() calls don't duplicate input
|
|
3
|
+
*
|
|
4
|
+
* To run this test:
|
|
5
|
+
* npx ts-node tests/manual/test-concurrent-ask.ts
|
|
6
|
+
*
|
|
7
|
+
* Expected behavior:
|
|
8
|
+
* 1. You should see "Question 2: What is your age?" displayed
|
|
9
|
+
* 2. As you type, each character should appear only once (not doubled)
|
|
10
|
+
* 3. When you press Enter, both promises resolve with the same answer
|
|
11
|
+
* 4. The output shows both questions got the same response
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ask } from "../../src/utils";
|
|
15
|
+
|
|
16
|
+
async function testConcurrentAsks() {
|
|
17
|
+
console.log("Testing concurrent ask() calls...\n");
|
|
18
|
+
console.log("This test asks two questions before you can answer.");
|
|
19
|
+
console.log("You should only see the second question and typing should not duplicate.\n");
|
|
20
|
+
|
|
21
|
+
// Start two asks without awaiting - simulates the collision scenario
|
|
22
|
+
const promise1 = ask("Question 1: What is your name? ");
|
|
23
|
+
|
|
24
|
+
// Simulate a small delay like an agent would have
|
|
25
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
26
|
+
|
|
27
|
+
const promise2 = ask("Question 2: What is your age? ");
|
|
28
|
+
|
|
29
|
+
// Now wait for both to resolve
|
|
30
|
+
const [answer1, answer2] = await Promise.all([promise1, promise2]);
|
|
31
|
+
|
|
32
|
+
console.log("\nResults:");
|
|
33
|
+
console.log("Answer to question 1:", answer1);
|
|
34
|
+
console.log("Answer to question 2:", answer2);
|
|
35
|
+
console.log("\nBoth questions should have received the same answer.");
|
|
36
|
+
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
testConcurrentAsks().catch(error => {
|
|
41
|
+
console.error("Error:", error);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DockerService } from "../../src/services/DockerService";
|
|
2
|
+
|
|
3
|
+
describe("DockerService", () => {
|
|
4
|
+
let dockerService: DockerService;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
dockerService = new DockerService();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe("checkDockerAvailable", () => {
|
|
11
|
+
it("should check if Docker is available", async () => {
|
|
12
|
+
const result = await dockerService.checkDockerAvailable();
|
|
13
|
+
// Result will be true or false depending on whether Docker is installed
|
|
14
|
+
expect(typeof result).toBe("boolean");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("imageExists", () => {
|
|
19
|
+
it("should check if the worker image exists", async () => {
|
|
20
|
+
const result = await dockerService.imageExists();
|
|
21
|
+
expect(typeof result).toBe("boolean");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|