@copilotkit/runtime 1.8.12-next.3 → 1.8.12-next.4
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/CHANGELOG.md +7 -0
- package/dist/{chunk-2L6VOEJ4.mjs → chunk-FA3E4I4W.mjs} +4 -3
- package/dist/chunk-FA3E4I4W.mjs.map +1 -0
- package/dist/{chunk-KVRZ3PWO.mjs → chunk-KGZF7KSR.mjs} +2 -2
- package/dist/{chunk-LZBWDTON.mjs → chunk-MG576PIZ.mjs} +2 -2
- package/dist/{chunk-Y4H3U52G.mjs → chunk-MVKCCH5U.mjs} +216 -173
- package/dist/chunk-MVKCCH5U.mjs.map +1 -0
- package/dist/{chunk-CF5VXJC6.mjs → chunk-S5U6J5X2.mjs} +2 -2
- package/dist/index.js +217 -173
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/lib/index.js +109 -82
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/index.mjs +5 -5
- package/dist/lib/integrations/index.js +2 -1
- package/dist/lib/integrations/index.js.map +1 -1
- package/dist/lib/integrations/index.mjs +5 -5
- package/dist/lib/integrations/nest/index.js +2 -1
- package/dist/lib/integrations/nest/index.js.map +1 -1
- package/dist/lib/integrations/nest/index.mjs +3 -3
- package/dist/lib/integrations/node-express/index.js +2 -1
- package/dist/lib/integrations/node-express/index.js.map +1 -1
- package/dist/lib/integrations/node-express/index.mjs +3 -3
- package/dist/lib/integrations/node-http/index.js +2 -1
- package/dist/lib/integrations/node-http/index.js.map +1 -1
- package/dist/lib/integrations/node-http/index.mjs +2 -2
- package/dist/service-adapters/index.js +215 -172
- package/dist/service-adapters/index.js.map +1 -1
- package/dist/service-adapters/index.mjs +1 -1
- package/jest.config.js +8 -3
- package/package.json +3 -2
- package/src/service-adapters/anthropic/anthropic-adapter.ts +124 -66
- package/src/service-adapters/anthropic/utils.ts +0 -19
- package/src/service-adapters/openai/openai-adapter.ts +107 -69
- package/tests/global.d.ts +13 -0
- package/tests/service-adapters/anthropic/allowlist-approach.test.ts +226 -0
- package/tests/service-adapters/anthropic/anthropic-adapter.test.ts +604 -0
- package/tests/service-adapters/openai/allowlist-approach.test.ts +238 -0
- package/tests/service-adapters/openai/openai-adapter.test.ts +301 -0
- package/tests/setup.jest.ts +21 -0
- package/tests/tsconfig.json +10 -0
- package/tsconfig.json +1 -1
- package/dist/chunk-2L6VOEJ4.mjs.map +0 -1
- package/dist/chunk-Y4H3U52G.mjs.map +0 -1
- /package/dist/{chunk-KVRZ3PWO.mjs.map → chunk-KGZF7KSR.mjs.map} +0 -0
- /package/dist/{chunk-LZBWDTON.mjs.map → chunk-MG576PIZ.mjs.map} +0 -0
- /package/dist/{chunk-CF5VXJC6.mjs.map → chunk-S5U6J5X2.mjs.map} +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "@jest/globals";
|
|
6
|
+
|
|
7
|
+
describe("OpenAI Adapter - Allowlist Approach", () => {
|
|
8
|
+
it("should filter out tool_result messages with no corresponding tool_call ID", () => {
|
|
9
|
+
// Setup test data
|
|
10
|
+
const validToolCallIds = new Set<string>(["valid-id-1", "valid-id-2"]);
|
|
11
|
+
|
|
12
|
+
// Messages to filter - valid and invalid ones
|
|
13
|
+
const messages = [
|
|
14
|
+
{ type: "text", role: "user", content: "Hello" },
|
|
15
|
+
{ type: "tool_result", actionExecutionId: "valid-id-1", result: "result1" },
|
|
16
|
+
{ type: "tool_result", actionExecutionId: "invalid-id", result: "invalid" },
|
|
17
|
+
{ type: "tool_result", actionExecutionId: "valid-id-2", result: "result2" },
|
|
18
|
+
{ type: "tool_result", actionExecutionId: "valid-id-1", result: "duplicate" }, // Duplicate ID
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Implement the filtering logic, similar to the adapter
|
|
22
|
+
const filteredMessages = messages.filter((message) => {
|
|
23
|
+
if (message.type === "tool_result") {
|
|
24
|
+
// Skip if there's no corresponding tool_call
|
|
25
|
+
if (!validToolCallIds.has(message.actionExecutionId)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Remove this ID from valid IDs so we don't process duplicates
|
|
30
|
+
validToolCallIds.delete(message.actionExecutionId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Keep all non-tool-result messages
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Verify results
|
|
38
|
+
expect(filteredMessages.length).toBe(3); // text + 2 valid tool results (no duplicates or invalid)
|
|
39
|
+
|
|
40
|
+
// Valid results should be included
|
|
41
|
+
expect(
|
|
42
|
+
filteredMessages.some(
|
|
43
|
+
(m) => m.type === "tool_result" && m.actionExecutionId === "valid-id-1",
|
|
44
|
+
),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
|
|
47
|
+
expect(
|
|
48
|
+
filteredMessages.some(
|
|
49
|
+
(m) => m.type === "tool_result" && m.actionExecutionId === "valid-id-2",
|
|
50
|
+
),
|
|
51
|
+
).toBe(true);
|
|
52
|
+
|
|
53
|
+
// Invalid result should be excluded
|
|
54
|
+
expect(
|
|
55
|
+
filteredMessages.some(
|
|
56
|
+
(m) => m.type === "tool_result" && m.actionExecutionId === "invalid-id",
|
|
57
|
+
),
|
|
58
|
+
).toBe(false);
|
|
59
|
+
|
|
60
|
+
// Duplicate should be excluded - we used a different approach than Anthropic
|
|
61
|
+
const validId1Count = filteredMessages.filter(
|
|
62
|
+
(m) => m.type === "tool_result" && m.actionExecutionId === "valid-id-1",
|
|
63
|
+
).length;
|
|
64
|
+
|
|
65
|
+
expect(validId1Count).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should maintain correct order of messages when filtering", () => {
|
|
69
|
+
// Setup test data
|
|
70
|
+
const validToolCallIds = new Set<string>(["tool-1", "tool-2", "tool-3"]);
|
|
71
|
+
|
|
72
|
+
// Test with a complex conversation pattern
|
|
73
|
+
const messages = [
|
|
74
|
+
{ type: "text", role: "user", content: "Initial message" },
|
|
75
|
+
{ type: "text", role: "assistant", content: "I'll help with that" },
|
|
76
|
+
{ type: "tool_call", id: "tool-1", name: "firstTool" },
|
|
77
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "result1" },
|
|
78
|
+
{ type: "text", role: "assistant", content: "Got the first result" },
|
|
79
|
+
{ type: "tool_call", id: "tool-2", name: "secondTool" },
|
|
80
|
+
{ type: "tool_result", actionExecutionId: "tool-2", result: "result2" },
|
|
81
|
+
{ type: "tool_result", actionExecutionId: "invalid-id", result: "invalid-result" },
|
|
82
|
+
{ type: "tool_call", id: "tool-3", name: "thirdTool" },
|
|
83
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "duplicate-result" }, // Duplicate
|
|
84
|
+
{ type: "tool_result", actionExecutionId: "tool-3", result: "result3" },
|
|
85
|
+
{ type: "text", role: "user", content: "Final message" },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Apply OpenAI's filter approach (using filter instead of loop)
|
|
89
|
+
const filteredMessages = messages.filter((message) => {
|
|
90
|
+
if (message.type === "tool_result") {
|
|
91
|
+
// Skip if there's no corresponding tool_call
|
|
92
|
+
if (!validToolCallIds.has(message.actionExecutionId)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Remove this ID from valid IDs so we don't process duplicates
|
|
97
|
+
validToolCallIds.delete(message.actionExecutionId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Keep all non-tool-result messages
|
|
101
|
+
return true;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Verify results
|
|
105
|
+
expect(filteredMessages.length).toBe(10); // 12 original - 2 filtered out
|
|
106
|
+
|
|
107
|
+
// Check that the message order is preserved
|
|
108
|
+
expect(filteredMessages[0].type).toBe("text"); // Initial user message
|
|
109
|
+
expect(filteredMessages[0].content).toBe("Initial message");
|
|
110
|
+
expect(filteredMessages[1].type).toBe("text"); // Assistant response
|
|
111
|
+
expect(filteredMessages[2].type).toBe("tool_call"); // First tool
|
|
112
|
+
expect(filteredMessages[3].type).toBe("tool_result"); // First result
|
|
113
|
+
expect(filteredMessages[3].actionExecutionId).toBe("tool-1");
|
|
114
|
+
expect(filteredMessages[4].type).toBe("text"); // Assistant comment
|
|
115
|
+
expect(filteredMessages[5].type).toBe("tool_call"); // Second tool
|
|
116
|
+
expect(filteredMessages[6].type).toBe("tool_result"); // Second result
|
|
117
|
+
expect(filteredMessages[6].actionExecutionId).toBe("tool-2");
|
|
118
|
+
expect(filteredMessages[7].type).toBe("tool_call"); // Third tool
|
|
119
|
+
expect(filteredMessages[8].type).toBe("tool_result"); // Third result
|
|
120
|
+
expect(filteredMessages[8].actionExecutionId).toBe("tool-3");
|
|
121
|
+
expect(filteredMessages[9].type).toBe("text"); // Final user message
|
|
122
|
+
|
|
123
|
+
// Each valid tool result should appear exactly once
|
|
124
|
+
const toolResultCounts = new Map();
|
|
125
|
+
filteredMessages.forEach((message) => {
|
|
126
|
+
if (message.type === "tool_result") {
|
|
127
|
+
const id = message.actionExecutionId;
|
|
128
|
+
toolResultCounts.set(id, (toolResultCounts.get(id) || 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(toolResultCounts.size).toBe(3); // Should have 3 different tool results
|
|
133
|
+
expect(toolResultCounts.get("tool-1")).toBe(1);
|
|
134
|
+
expect(toolResultCounts.get("tool-2")).toBe(1);
|
|
135
|
+
expect(toolResultCounts.get("tool-3")).toBe(1);
|
|
136
|
+
expect(toolResultCounts.has("invalid-id")).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should handle empty message array", () => {
|
|
140
|
+
const validToolCallIds = new Set<string>(["valid-id-1", "valid-id-2"]);
|
|
141
|
+
const messages = [];
|
|
142
|
+
|
|
143
|
+
// Apply OpenAI's filter approach
|
|
144
|
+
const filteredMessages = messages.filter((message) => {
|
|
145
|
+
if (message.type === "tool_result") {
|
|
146
|
+
if (!validToolCallIds.has(message.actionExecutionId)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
validToolCallIds.delete(message.actionExecutionId);
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(filteredMessages.length).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should handle edge cases with mixed message types", () => {
|
|
158
|
+
// Setup test data with various message types
|
|
159
|
+
const validToolCallIds = new Set<string>(["valid-id-1"]);
|
|
160
|
+
|
|
161
|
+
const messages = [
|
|
162
|
+
{ type: "text", role: "user", content: "Hello" },
|
|
163
|
+
{ type: "image", url: "https://example.com/image.jpg" }, // Non-tool message type
|
|
164
|
+
{ type: "tool_result", actionExecutionId: "valid-id-1", result: "result1" },
|
|
165
|
+
{ type: "custom", data: { key: "value" } }, // Another custom type
|
|
166
|
+
{ type: "tool_result", actionExecutionId: "valid-id-1", result: "duplicate" }, // Duplicate
|
|
167
|
+
{ type: "null", value: null }, // Edge case
|
|
168
|
+
{ type: "undefined" }, // Edge case
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
// Apply OpenAI's filter approach
|
|
172
|
+
const filteredMessages = messages.filter((message) => {
|
|
173
|
+
if (message.type === "tool_result") {
|
|
174
|
+
if (!validToolCallIds.has(message.actionExecutionId)) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
validToolCallIds.delete(message.actionExecutionId);
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Should have all non-tool_result messages + 1 valid tool_result
|
|
183
|
+
expect(filteredMessages.length).toBe(6); // 7 original - 1 duplicate
|
|
184
|
+
|
|
185
|
+
// Valid tool_result should be included exactly once
|
|
186
|
+
const toolResults = filteredMessages.filter((m) => m.type === "tool_result");
|
|
187
|
+
expect(toolResults.length).toBe(1);
|
|
188
|
+
expect(toolResults[0].actionExecutionId).toBe("valid-id-1");
|
|
189
|
+
|
|
190
|
+
// All non-tool_result messages should be preserved
|
|
191
|
+
expect(filteredMessages.filter((m) => m.type === "text").length).toBe(1);
|
|
192
|
+
expect(filteredMessages.filter((m) => m.type === "image").length).toBe(1);
|
|
193
|
+
expect(filteredMessages.filter((m) => m.type === "custom").length).toBe(1);
|
|
194
|
+
expect(filteredMessages.filter((m) => m.type === "null").length).toBe(1);
|
|
195
|
+
expect(filteredMessages.filter((m) => m.type === "undefined").length).toBe(1);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should properly handle multiple duplicate tool results", () => {
|
|
199
|
+
// Setup test data with multiple duplicates
|
|
200
|
+
const validToolCallIds = new Set<string>(["tool-1", "tool-2"]);
|
|
201
|
+
|
|
202
|
+
const messages = [
|
|
203
|
+
{ type: "text", role: "user", content: "Initial prompt" },
|
|
204
|
+
{ type: "tool_call", id: "tool-1", name: "firstTool" },
|
|
205
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "first-result" },
|
|
206
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "duplicate-1" }, // Duplicate 1
|
|
207
|
+
{ type: "tool_call", id: "tool-2", name: "secondTool" },
|
|
208
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "duplicate-2" }, // Duplicate 2
|
|
209
|
+
{ type: "tool_result", actionExecutionId: "tool-2", result: "second-result" },
|
|
210
|
+
{ type: "tool_result", actionExecutionId: "tool-2", result: "duplicate-3" }, // Duplicate 3
|
|
211
|
+
{ type: "tool_result", actionExecutionId: "tool-1", result: "duplicate-4" }, // Duplicate 4
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
// Apply OpenAI's filter approach
|
|
215
|
+
const filteredMessages = messages.filter((message) => {
|
|
216
|
+
if (message.type === "tool_result") {
|
|
217
|
+
if (!validToolCallIds.has(message.actionExecutionId)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
validToolCallIds.delete(message.actionExecutionId);
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Should have text + tool calls + only the first occurrence of each tool result
|
|
226
|
+
expect(filteredMessages.length).toBe(5);
|
|
227
|
+
|
|
228
|
+
// Check that only the first occurrence of each tool result is kept
|
|
229
|
+
const toolResults = filteredMessages.filter((m) => m.type === "tool_result");
|
|
230
|
+
expect(toolResults.length).toBe(2);
|
|
231
|
+
|
|
232
|
+
expect(toolResults[0].actionExecutionId).toBe("tool-1");
|
|
233
|
+
expect(toolResults[0].result).toBe("first-result"); // First occurrence should be kept
|
|
234
|
+
|
|
235
|
+
expect(toolResults[1].actionExecutionId).toBe("tool-2");
|
|
236
|
+
expect(toolResults[1].result).toBe("second-result"); // First occurrence should be kept
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// Mock the modules first
|
|
2
|
+
jest.mock("openai", () => {
|
|
3
|
+
function MockOpenAI() {}
|
|
4
|
+
return { default: MockOpenAI };
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
// Mock the OpenAIAdapter class to avoid the "new OpenAI()" issue
|
|
8
|
+
jest.mock("../../../src/service-adapters/openai/openai-adapter", () => {
|
|
9
|
+
class MockOpenAIAdapter {
|
|
10
|
+
_openai: any;
|
|
11
|
+
model: string = "gpt-4o";
|
|
12
|
+
keepSystemRole: boolean = false;
|
|
13
|
+
disableParallelToolCalls: boolean = false;
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
this._openai = {
|
|
17
|
+
beta: {
|
|
18
|
+
chat: {
|
|
19
|
+
completions: {
|
|
20
|
+
stream: jest.fn(),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get openai() {
|
|
28
|
+
return this._openai;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async process(request: any) {
|
|
32
|
+
// Mock implementation that calls our event source but doesn't do the actual processing
|
|
33
|
+
request.eventSource.stream(async (stream: any) => {
|
|
34
|
+
stream.complete();
|
|
35
|
+
return Promise.resolve();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return { threadId: request.threadId || "mock-thread-id" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { OpenAIAdapter: MockOpenAIAdapter };
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Now import the modules
|
|
46
|
+
import { OpenAIAdapter } from "../../../src/service-adapters/openai/openai-adapter";
|
|
47
|
+
|
|
48
|
+
// Mock the Message classes since they use TypeGraphQL decorators
|
|
49
|
+
jest.mock("../../../src/graphql/types/converted", () => {
|
|
50
|
+
// Create minimal implementations of the message classes
|
|
51
|
+
class MockTextMessage {
|
|
52
|
+
content: string;
|
|
53
|
+
role: string;
|
|
54
|
+
id: string;
|
|
55
|
+
|
|
56
|
+
constructor(role: string, content: string) {
|
|
57
|
+
this.role = role;
|
|
58
|
+
this.content = content;
|
|
59
|
+
this.id = "mock-text-" + Math.random().toString(36).substring(7);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
isTextMessage() {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
isImageMessage() {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
isActionExecutionMessage() {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
isResultMessage() {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class MockActionExecutionMessage {
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
arguments: string;
|
|
80
|
+
|
|
81
|
+
constructor(params: { id: string; name: string; arguments: string }) {
|
|
82
|
+
this.id = params.id;
|
|
83
|
+
this.name = params.name;
|
|
84
|
+
this.arguments = params.arguments;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isTextMessage() {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
isImageMessage() {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
isActionExecutionMessage() {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
isResultMessage() {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class MockResultMessage {
|
|
102
|
+
actionExecutionId: string;
|
|
103
|
+
result: string;
|
|
104
|
+
id: string;
|
|
105
|
+
|
|
106
|
+
constructor(params: { actionExecutionId: string; result: string }) {
|
|
107
|
+
this.actionExecutionId = params.actionExecutionId;
|
|
108
|
+
this.result = params.result;
|
|
109
|
+
this.id = "mock-result-" + Math.random().toString(36).substring(7);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
isTextMessage() {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
isImageMessage() {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
isActionExecutionMessage() {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
isResultMessage() {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
TextMessage: MockTextMessage,
|
|
128
|
+
ActionExecutionMessage: MockActionExecutionMessage,
|
|
129
|
+
ResultMessage: MockResultMessage,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("OpenAIAdapter", () => {
|
|
134
|
+
let adapter: OpenAIAdapter;
|
|
135
|
+
let mockEventSource: any;
|
|
136
|
+
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
jest.clearAllMocks();
|
|
139
|
+
adapter = new OpenAIAdapter();
|
|
140
|
+
mockEventSource = {
|
|
141
|
+
stream: jest.fn((callback) => {
|
|
142
|
+
const mockStream = {
|
|
143
|
+
sendTextMessageStart: jest.fn(),
|
|
144
|
+
sendTextMessageContent: jest.fn(),
|
|
145
|
+
sendTextMessageEnd: jest.fn(),
|
|
146
|
+
sendActionExecutionStart: jest.fn(),
|
|
147
|
+
sendActionExecutionArgs: jest.fn(),
|
|
148
|
+
sendActionExecutionEnd: jest.fn(),
|
|
149
|
+
complete: jest.fn(),
|
|
150
|
+
};
|
|
151
|
+
callback(mockStream);
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("Tool ID handling", () => {
|
|
157
|
+
it("should filter out tool_result messages that don't have corresponding tool_call IDs", async () => {
|
|
158
|
+
// Import dynamically after mocking
|
|
159
|
+
const {
|
|
160
|
+
TextMessage,
|
|
161
|
+
ActionExecutionMessage,
|
|
162
|
+
ResultMessage,
|
|
163
|
+
} = require("../../../src/graphql/types/converted");
|
|
164
|
+
|
|
165
|
+
// Create messages including one valid pair and one invalid tool_result
|
|
166
|
+
const systemMessage = new TextMessage("system", "System message");
|
|
167
|
+
const userMessage = new TextMessage("user", "User message");
|
|
168
|
+
|
|
169
|
+
// Valid tool execution message
|
|
170
|
+
const validToolExecution = new ActionExecutionMessage({
|
|
171
|
+
id: "valid-tool-id",
|
|
172
|
+
name: "validTool",
|
|
173
|
+
arguments: '{"arg":"value"}',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Valid result for the above tool
|
|
177
|
+
const validToolResult = new ResultMessage({
|
|
178
|
+
actionExecutionId: "valid-tool-id",
|
|
179
|
+
result: '{"result":"success"}',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Invalid tool result with no corresponding tool execution
|
|
183
|
+
const invalidToolResult = new ResultMessage({
|
|
184
|
+
actionExecutionId: "invalid-tool-id",
|
|
185
|
+
result: '{"result":"failure"}',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Spy on the process method to test it's called properly
|
|
189
|
+
const processSpy = jest.spyOn(adapter, "process");
|
|
190
|
+
|
|
191
|
+
await adapter.process({
|
|
192
|
+
threadId: "test-thread",
|
|
193
|
+
model: "gpt-4o",
|
|
194
|
+
messages: [
|
|
195
|
+
systemMessage,
|
|
196
|
+
userMessage,
|
|
197
|
+
validToolExecution,
|
|
198
|
+
validToolResult,
|
|
199
|
+
invalidToolResult,
|
|
200
|
+
],
|
|
201
|
+
actions: [],
|
|
202
|
+
eventSource: mockEventSource,
|
|
203
|
+
forwardedParameters: {},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Verify the process method was called
|
|
207
|
+
expect(processSpy).toHaveBeenCalledTimes(1);
|
|
208
|
+
|
|
209
|
+
// Verify the stream function was called
|
|
210
|
+
expect(mockEventSource.stream).toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should handle duplicate tool IDs by only using each once", async () => {
|
|
214
|
+
// Import dynamically after mocking
|
|
215
|
+
const {
|
|
216
|
+
TextMessage,
|
|
217
|
+
ActionExecutionMessage,
|
|
218
|
+
ResultMessage,
|
|
219
|
+
} = require("../../../src/graphql/types/converted");
|
|
220
|
+
|
|
221
|
+
// Create messages including duplicate tool results for the same ID
|
|
222
|
+
const systemMessage = new TextMessage("system", "System message");
|
|
223
|
+
|
|
224
|
+
// Valid tool execution message
|
|
225
|
+
const toolExecution = new ActionExecutionMessage({
|
|
226
|
+
id: "tool-id-1",
|
|
227
|
+
name: "someTool",
|
|
228
|
+
arguments: '{"arg":"value"}',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Two results for the same tool ID
|
|
232
|
+
const firstToolResult = new ResultMessage({
|
|
233
|
+
actionExecutionId: "tool-id-1",
|
|
234
|
+
result: '{"result":"first"}',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const duplicateToolResult = new ResultMessage({
|
|
238
|
+
actionExecutionId: "tool-id-1",
|
|
239
|
+
result: '{"result":"duplicate"}',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Spy on the process method to test it's called properly
|
|
243
|
+
const processSpy = jest.spyOn(adapter, "process");
|
|
244
|
+
|
|
245
|
+
await adapter.process({
|
|
246
|
+
threadId: "test-thread",
|
|
247
|
+
model: "gpt-4o",
|
|
248
|
+
messages: [systemMessage, toolExecution, firstToolResult, duplicateToolResult],
|
|
249
|
+
actions: [],
|
|
250
|
+
eventSource: mockEventSource,
|
|
251
|
+
forwardedParameters: {},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Verify the process method was called
|
|
255
|
+
expect(processSpy).toHaveBeenCalledTimes(1);
|
|
256
|
+
|
|
257
|
+
// Verify the stream function was called
|
|
258
|
+
expect(mockEventSource.stream).toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should call the stream method on eventSource", async () => {
|
|
262
|
+
// Import dynamically after mocking
|
|
263
|
+
const { TextMessage } = require("../../../src/graphql/types/converted");
|
|
264
|
+
|
|
265
|
+
// Create messages
|
|
266
|
+
const systemMessage = new TextMessage("system", "System message");
|
|
267
|
+
const userMessage = new TextMessage("user", "User message");
|
|
268
|
+
|
|
269
|
+
await adapter.process({
|
|
270
|
+
threadId: "test-thread",
|
|
271
|
+
model: "gpt-4o",
|
|
272
|
+
messages: [systemMessage, userMessage],
|
|
273
|
+
actions: [],
|
|
274
|
+
eventSource: mockEventSource,
|
|
275
|
+
forwardedParameters: {},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Verify the stream function was called
|
|
279
|
+
expect(mockEventSource.stream).toHaveBeenCalled();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("should return the provided threadId", async () => {
|
|
283
|
+
// Import dynamically after mocking
|
|
284
|
+
const { TextMessage } = require("../../../src/graphql/types/converted");
|
|
285
|
+
|
|
286
|
+
// Create a message
|
|
287
|
+
const systemMessage = new TextMessage("system", "System message");
|
|
288
|
+
|
|
289
|
+
const result = await adapter.process({
|
|
290
|
+
threadId: "test-thread",
|
|
291
|
+
model: "gpt-4o",
|
|
292
|
+
messages: [systemMessage],
|
|
293
|
+
actions: [],
|
|
294
|
+
eventSource: mockEventSource,
|
|
295
|
+
forwardedParameters: {},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(result.threadId).toBe("test-thread");
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Import reflect-metadata to support TypeGraphQL
|
|
2
|
+
import "reflect-metadata";
|
|
3
|
+
|
|
4
|
+
// Import Jest types and functions
|
|
5
|
+
import {
|
|
6
|
+
jest,
|
|
7
|
+
describe,
|
|
8
|
+
expect,
|
|
9
|
+
it,
|
|
10
|
+
test,
|
|
11
|
+
beforeEach,
|
|
12
|
+
afterEach,
|
|
13
|
+
beforeAll,
|
|
14
|
+
afterAll,
|
|
15
|
+
} from "@jest/globals";
|
|
16
|
+
|
|
17
|
+
// Suppress console output during tests
|
|
18
|
+
jest.spyOn(console, "log").mockImplementation(() => {});
|
|
19
|
+
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
20
|
+
|
|
21
|
+
// The global types are already declared in global.d.ts, so we don't need to set globals here
|
package/tsconfig.json
CHANGED
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
"strict": false,
|
|
9
9
|
"resolveJsonModule": true
|
|
10
10
|
},
|
|
11
|
-
"include": ["./src/**/*.ts", "./src/**/*.test.ts", "./src/**/__tests__/*"],
|
|
11
|
+
"include": ["./src/**/*.ts", "./src/**/*.test.ts", "./src/**/__tests__/*", "./tests/**/*.ts"],
|
|
12
12
|
"exclude": ["dist", "build", "node_modules"]
|
|
13
13
|
}
|