@xalia/agent 0.6.1 → 0.6.2
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/dist/agent/src/agent/agent.js +103 -54
- package/dist/agent/src/agent/agentUtils.js +22 -21
- package/dist/agent/src/agent/compressingContextManager.js +3 -2
- package/dist/agent/src/agent/dummyLLM.js +1 -3
- package/dist/agent/src/agent/imageGenLLM.js +67 -0
- package/dist/agent/src/agent/imageGenerator.js +43 -0
- package/dist/agent/src/agent/llm.js +27 -0
- package/dist/agent/src/agent/mcpServerManager.js +18 -6
- package/dist/agent/src/agent/nullAgentEventHandler.js +6 -0
- package/dist/agent/src/agent/openAILLM.js +3 -3
- package/dist/agent/src/agent/openAILLMStreaming.js +41 -6
- package/dist/agent/src/chat/client/chatClient.js +84 -13
- package/dist/agent/src/chat/client/sessionClient.js +47 -6
- package/dist/agent/src/chat/client/sessionFiles.js +102 -0
- package/dist/agent/src/chat/data/apiKeyManager.js +38 -7
- package/dist/agent/src/chat/data/database.js +83 -70
- package/dist/agent/src/chat/data/dbSessionFileModels.js +49 -0
- package/dist/agent/src/chat/data/dbSessionFiles.js +76 -0
- package/dist/agent/src/chat/data/dbSessionMessages.js +57 -0
- package/dist/agent/src/chat/data/mimeTypes.js +44 -0
- package/dist/agent/src/chat/protocol/messages.js +21 -0
- package/dist/agent/src/chat/server/chatContextManager.js +14 -7
- package/dist/agent/src/chat/server/connectionManager.js +14 -36
- package/dist/agent/src/chat/server/connectionManager.test.js +2 -16
- package/dist/agent/src/chat/server/conversation.js +69 -45
- package/dist/agent/src/chat/server/imageGeneratorTools.js +111 -0
- package/dist/agent/src/chat/server/openSession.js +205 -43
- package/dist/agent/src/chat/server/server.js +5 -8
- package/dist/agent/src/chat/server/sessionFileManager.js +171 -38
- package/dist/agent/src/chat/server/sessionRegistry.js +199 -32
- package/dist/agent/src/chat/server/test-utils/mockFactories.js +12 -11
- package/dist/agent/src/chat/server/tools.js +27 -6
- package/dist/agent/src/chat/utils/multiAsyncQueue.js +9 -1
- package/dist/agent/src/test/agent.test.js +15 -11
- package/dist/agent/src/test/chatContextManager.test.js +4 -0
- package/dist/agent/src/test/clientServerConnection.test.js +2 -2
- package/dist/agent/src/test/db.test.js +33 -70
- package/dist/agent/src/test/dbSessionFiles.test.js +179 -0
- package/dist/agent/src/test/dbSessionMessages.test.js +67 -0
- package/dist/agent/src/test/dbTestTools.js +6 -5
- package/dist/agent/src/test/imageLoad.test.js +1 -1
- package/dist/agent/src/test/mcpServerManager.test.js +1 -1
- package/dist/agent/src/test/multiAsyncQueue.test.js +50 -0
- package/dist/agent/src/test/testTools.js +12 -0
- package/dist/agent/src/tool/agentChat.js +25 -6
- package/dist/agent/src/tool/agentMain.js +1 -1
- package/dist/agent/src/tool/chatMain.js +113 -4
- package/dist/agent/src/tool/commandPrompt.js +7 -3
- package/dist/agent/src/tool/files.js +23 -15
- package/dist/agent/src/tool/options.js +2 -2
- package/package.json +1 -1
- package/scripts/test_chat +124 -66
- package/src/agent/agent.ts +145 -38
- package/src/agent/agentUtils.ts +27 -21
- package/src/agent/compressingContextManager.ts +5 -4
- package/src/agent/context.ts +1 -1
- package/src/agent/dummyLLM.ts +1 -3
- package/src/agent/iAgentEventHandler.ts +15 -2
- package/src/agent/imageGenLLM.ts +99 -0
- package/src/agent/imageGenerator.ts +60 -0
- package/src/agent/llm.ts +128 -4
- package/src/agent/mcpServerManager.ts +26 -7
- package/src/agent/nullAgentEventHandler.ts +6 -0
- package/src/agent/openAILLM.ts +3 -8
- package/src/agent/openAILLMStreaming.ts +60 -14
- package/src/chat/client/chatClient.ts +119 -14
- package/src/chat/client/sessionClient.ts +75 -9
- package/src/chat/client/sessionFiles.ts +145 -0
- package/src/chat/data/apiKeyManager.ts +55 -7
- package/src/chat/data/dataModels.ts +16 -7
- package/src/chat/data/database.ts +107 -92
- package/src/chat/data/dbSessionFileModels.ts +91 -0
- package/src/chat/data/dbSessionFiles.ts +99 -0
- package/src/chat/data/dbSessionMessages.ts +68 -0
- package/src/chat/data/mimeTypes.ts +58 -0
- package/src/chat/protocol/messages.ts +127 -13
- package/src/chat/server/chatContextManager.ts +36 -13
- package/src/chat/server/connectionManager.test.ts +1 -22
- package/src/chat/server/connectionManager.ts +18 -53
- package/src/chat/server/conversation.ts +96 -57
- package/src/chat/server/imageGeneratorTools.ts +138 -0
- package/src/chat/server/openSession.ts +287 -49
- package/src/chat/server/server.ts +5 -11
- package/src/chat/server/sessionFileManager.ts +223 -63
- package/src/chat/server/sessionRegistry.ts +285 -41
- package/src/chat/server/test-utils/mockFactories.ts +13 -13
- package/src/chat/server/tools.ts +43 -8
- package/src/chat/utils/agentSessionMap.ts +2 -2
- package/src/chat/utils/multiAsyncQueue.ts +11 -1
- package/src/test/agent.test.ts +23 -14
- package/src/test/chatContextManager.test.ts +7 -2
- package/src/test/clientServerConnection.test.ts +3 -3
- package/src/test/compressingContextManager.test.ts +1 -1
- package/src/test/context.test.ts +2 -1
- package/src/test/conversation.test.ts +1 -1
- package/src/test/db.test.ts +41 -83
- package/src/test/dbSessionFiles.test.ts +258 -0
- package/src/test/dbSessionMessages.test.ts +85 -0
- package/src/test/dbTestTools.ts +9 -5
- package/src/test/imageLoad.test.ts +2 -2
- package/src/test/mcpServerManager.test.ts +3 -1
- package/src/test/multiAsyncQueue.test.ts +58 -0
- package/src/test/testTools.ts +15 -1
- package/src/tool/agentChat.ts +35 -7
- package/src/tool/agentMain.ts +7 -7
- package/src/tool/chatMain.ts +126 -5
- package/src/tool/commandPrompt.ts +10 -5
- package/src/tool/files.ts +30 -13
- package/src/tool/options.ts +1 -1
- package/test_data/dummyllm_script_image_gen.json +19 -0
- package/test_data/dummyllm_script_invoke_image_gen_tool.json +30 -0
- package/test_data/image_gen_test_profile.json +5 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
|
|
3
|
+
import { Database } from "../chat/data/database";
|
|
4
|
+
import { DbSessionFiles } from "../chat/data/dbSessionFiles";
|
|
5
|
+
import {
|
|
6
|
+
SessionFileDescriptor,
|
|
7
|
+
SessionFileEntry,
|
|
8
|
+
} from "../chat/data/dbSessionFileModels";
|
|
9
|
+
import {
|
|
10
|
+
TestUserSpec,
|
|
11
|
+
cleanupTestUser,
|
|
12
|
+
createTestSession,
|
|
13
|
+
getLocalDB,
|
|
14
|
+
testUserSpec,
|
|
15
|
+
} from "./dbTestTools";
|
|
16
|
+
import { getMimeTypeFromDataUrl } from "../chat/data/mimeTypes";
|
|
17
|
+
|
|
18
|
+
const TEST_FILES_A: SessionFileEntry[] = [
|
|
19
|
+
{
|
|
20
|
+
name: "file_1",
|
|
21
|
+
summary: "Some PDF",
|
|
22
|
+
mime_type: "application/pdf",
|
|
23
|
+
data_url: "data:application/pdf;base64,AAAA",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "file_2",
|
|
27
|
+
summary: "Some PNG",
|
|
28
|
+
mime_type: "image/png",
|
|
29
|
+
data_url: "data:image/png;base64,BBBB",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const TEST_FILE_A_1_ALT: SessionFileEntry = {
|
|
34
|
+
...TEST_FILES_A[0],
|
|
35
|
+
summary: "Some other PDF",
|
|
36
|
+
data_url: "data:application/pdf;base64,CCCC",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const TEST_FILE_A_3_NEW: SessionFileEntry = {
|
|
40
|
+
name: "file_3",
|
|
41
|
+
summary: "Some markdown",
|
|
42
|
+
mime_type: "text/markdown",
|
|
43
|
+
data_url: "data:text/markdown,# TITLE\n## SUBSECTION\nA parapgraph\n",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const TEST_FILES_B: SessionFileEntry[] = [
|
|
47
|
+
{
|
|
48
|
+
name: "file_1",
|
|
49
|
+
summary: "Some other PDF",
|
|
50
|
+
mime_type: "application/pdf",
|
|
51
|
+
data_url: "data:application/pdf;base64,DDD",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function entryToDescriptor(entry: SessionFileEntry): SessionFileDescriptor {
|
|
56
|
+
return {
|
|
57
|
+
name: entry.name,
|
|
58
|
+
summary: entry.summary,
|
|
59
|
+
mime_type: entry.mime_type,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// async function cleanup(
|
|
64
|
+
// db: Database,
|
|
65
|
+
// user1: TestUserSpec
|
|
66
|
+
// // user2: TestUser
|
|
67
|
+
// ): Promise<void> {
|
|
68
|
+
// await cleanupTestUser(db, user1);
|
|
69
|
+
// // await cleanupTestUser(db, user2.uuid, user2.api_key);
|
|
70
|
+
// }
|
|
71
|
+
|
|
72
|
+
async function setup(
|
|
73
|
+
db: Database,
|
|
74
|
+
user1: TestUserSpec
|
|
75
|
+
): Promise<{
|
|
76
|
+
sf: DbSessionFiles;
|
|
77
|
+
session_id_A: string;
|
|
78
|
+
session_id_B: string;
|
|
79
|
+
}> {
|
|
80
|
+
const { agentProfileId, sessionId: session_id_A } = await createTestSession(
|
|
81
|
+
db,
|
|
82
|
+
user1
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const session_id_B = Database.sessionNewUUID();
|
|
86
|
+
await db.sessionCreate({
|
|
87
|
+
session_uuid: session_id_B,
|
|
88
|
+
user_uuid: user1.uuid,
|
|
89
|
+
title: "test_session_2",
|
|
90
|
+
agent_profile_uuid: agentProfileId,
|
|
91
|
+
agent_paused: false,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const sf = db.createTypedClient(DbSessionFiles);
|
|
95
|
+
|
|
96
|
+
await Promise.all(
|
|
97
|
+
TEST_FILES_A.map((tf) =>
|
|
98
|
+
sf.setFileContent(session_id_A, tf.name, tf.summary, tf.data_url)
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
await Promise.all(
|
|
102
|
+
TEST_FILES_B.map((tf) =>
|
|
103
|
+
sf.setFileContent(session_id_B, tf.name, tf.summary, tf.data_url)
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return { sf, session_id_A, session_id_B };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function expectArraysEqual<T extends { name: string }>(a: T[], b: T[]): void {
|
|
111
|
+
const sortByName = (a: { name: string }, b: { name: string }) =>
|
|
112
|
+
a.name.localeCompare(b.name);
|
|
113
|
+
expect(a.sort(sortByName)).eql(b.sort(sortByName));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe("DB Session Files", () => {
|
|
117
|
+
let db: Database;
|
|
118
|
+
let user: TestUserSpec;
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
db = getLocalDB();
|
|
122
|
+
user = testUserSpec();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
afterEach(async () => {
|
|
126
|
+
await cleanupTestUser(db, user);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("extract mime type from data url", function () {
|
|
130
|
+
const mimeTypeA = getMimeTypeFromDataUrl("data:image/png;base64,AAAAA");
|
|
131
|
+
const mimeTypeB = getMimeTypeFromDataUrl("data:text/markdown,# TITLE ...");
|
|
132
|
+
|
|
133
|
+
expect(mimeTypeA).eql("image/png");
|
|
134
|
+
expect(mimeTypeB).eql("text/markdown");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("listing entries and getting data", async function () {
|
|
138
|
+
const { sf, session_id_A, session_id_B } = await setup(db, user);
|
|
139
|
+
|
|
140
|
+
const [list_A, list_B] = await Promise.all([
|
|
141
|
+
sf.getFilesForSession(session_id_A),
|
|
142
|
+
sf.getFilesForSession(session_id_B),
|
|
143
|
+
]);
|
|
144
|
+
const [content_A_1, content_A_2, content_B_1] = await Promise.all([
|
|
145
|
+
sf.getFileContent(session_id_A, TEST_FILES_A[0].name),
|
|
146
|
+
sf.getFileContent(session_id_A, TEST_FILES_A[1].name),
|
|
147
|
+
sf.getFileContent(session_id_B, TEST_FILES_B[0].name),
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
expectArraysEqual(list_A, TEST_FILES_A.map(entryToDescriptor));
|
|
151
|
+
expectArraysEqual(list_B, TEST_FILES_B.map(entryToDescriptor));
|
|
152
|
+
expect(content_A_1).eql(TEST_FILES_A[0].data_url);
|
|
153
|
+
expect(content_A_2).eql(TEST_FILES_A[1].data_url);
|
|
154
|
+
expect(content_B_1).eql(TEST_FILES_B[0].data_url);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("deleting files", async function () {
|
|
158
|
+
const { sf, session_id_A, session_id_B } = await setup(db, user);
|
|
159
|
+
|
|
160
|
+
const [list_A_2, list_B_2] = await Promise.all([
|
|
161
|
+
sf.getFilesForSession(session_id_A),
|
|
162
|
+
sf.getFilesForSession(session_id_B),
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
// Should do nothing
|
|
166
|
+
await sf.deleteFile(session_id_B, TEST_FILES_A[1].name);
|
|
167
|
+
|
|
168
|
+
const [list_A_0, list_B_0] = await Promise.all([
|
|
169
|
+
sf.getFilesForSession(session_id_A),
|
|
170
|
+
sf.getFilesForSession(session_id_B),
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
// Should delete a file
|
|
174
|
+
await sf.deleteFile(session_id_A, TEST_FILES_A[0].name);
|
|
175
|
+
|
|
176
|
+
const [list_A_1, list_B_1] = await Promise.all([
|
|
177
|
+
sf.getFilesForSession(session_id_A),
|
|
178
|
+
sf.getFilesForSession(session_id_B),
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
expectArraysEqual(list_A_2, TEST_FILES_A.map(entryToDescriptor));
|
|
182
|
+
expectArraysEqual(list_B_2, TEST_FILES_B.map(entryToDescriptor));
|
|
183
|
+
expectArraysEqual(list_A_0, TEST_FILES_A.map(entryToDescriptor));
|
|
184
|
+
expectArraysEqual(list_B_0, TEST_FILES_B.map(entryToDescriptor));
|
|
185
|
+
expectArraysEqual(list_A_1, TEST_FILES_A.slice(1).map(entryToDescriptor));
|
|
186
|
+
expectArraysEqual(list_B_1, TEST_FILES_B.map(entryToDescriptor));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("deleting all", async function () {
|
|
190
|
+
const { sf, session_id_A, session_id_B } = await setup(db, user);
|
|
191
|
+
|
|
192
|
+
await sf.clearFiles(session_id_A);
|
|
193
|
+
|
|
194
|
+
const [list_A, list_B] = await Promise.all([
|
|
195
|
+
sf.getFilesForSession(session_id_A),
|
|
196
|
+
sf.getFilesForSession(session_id_B),
|
|
197
|
+
]);
|
|
198
|
+
expectArraysEqual(list_A, []);
|
|
199
|
+
expectArraysEqual(list_B, TEST_FILES_B.map(entryToDescriptor));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("file updates", async function () {
|
|
203
|
+
const { sf, session_id_A, session_id_B } = await setup(db, user);
|
|
204
|
+
|
|
205
|
+
await sf.setFileContent(
|
|
206
|
+
session_id_A,
|
|
207
|
+
TEST_FILE_A_1_ALT.name,
|
|
208
|
+
TEST_FILE_A_1_ALT.summary,
|
|
209
|
+
TEST_FILE_A_1_ALT.data_url
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const expectFiles_A = [TEST_FILE_A_1_ALT, TEST_FILES_A[1]];
|
|
213
|
+
|
|
214
|
+
const list_A = await sf.getFilesForSession(session_id_A);
|
|
215
|
+
const list_B = await sf.getFilesForSession(session_id_B);
|
|
216
|
+
const [content_A_1, content_A_2, content_B_1] = await Promise.all([
|
|
217
|
+
sf.getFileContent(session_id_A, TEST_FILES_A[0].name),
|
|
218
|
+
sf.getFileContent(session_id_A, TEST_FILES_A[1].name),
|
|
219
|
+
sf.getFileContent(session_id_B, TEST_FILES_B[0].name),
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
expectArraysEqual(list_A, expectFiles_A.map(entryToDescriptor));
|
|
223
|
+
expectArraysEqual(list_B, TEST_FILES_B.map(entryToDescriptor));
|
|
224
|
+
expect(content_A_1).eql(TEST_FILE_A_1_ALT.data_url);
|
|
225
|
+
expect(content_A_2).eql(TEST_FILES_A[1].data_url);
|
|
226
|
+
expect(content_B_1).eql(TEST_FILES_B[0].data_url);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("adding new files", async function () {
|
|
230
|
+
const { sf, session_id_A, session_id_B } = await setup(db, user);
|
|
231
|
+
|
|
232
|
+
await sf.setFileContent(
|
|
233
|
+
session_id_A,
|
|
234
|
+
TEST_FILE_A_3_NEW.name,
|
|
235
|
+
TEST_FILE_A_3_NEW.summary,
|
|
236
|
+
TEST_FILE_A_3_NEW.data_url
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const list_A = await sf.getFilesForSession(session_id_A);
|
|
240
|
+
const list_B = await sf.getFilesForSession(session_id_B);
|
|
241
|
+
const [content_A_1, content_A_2, content_A_3, content_B_1] =
|
|
242
|
+
await Promise.all([
|
|
243
|
+
sf.getFileContent(session_id_A, TEST_FILES_A[0].name),
|
|
244
|
+
sf.getFileContent(session_id_A, TEST_FILES_A[1].name),
|
|
245
|
+
sf.getFileContent(session_id_A, TEST_FILE_A_3_NEW.name),
|
|
246
|
+
sf.getFileContent(session_id_B, TEST_FILES_B[0].name),
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
const expectFiles_A = [...TEST_FILES_A, TEST_FILE_A_3_NEW];
|
|
250
|
+
|
|
251
|
+
expectArraysEqual(list_A, expectFiles_A.map(entryToDescriptor));
|
|
252
|
+
expectArraysEqual(list_B, TEST_FILES_B.map(entryToDescriptor));
|
|
253
|
+
expect(content_A_1).eql(TEST_FILES_A[0].data_url);
|
|
254
|
+
expect(content_A_2).eql(TEST_FILES_A[1].data_url);
|
|
255
|
+
expect(content_A_3).eql(TEST_FILE_A_3_NEW.data_url);
|
|
256
|
+
expect(content_B_1).eql(TEST_FILES_B[0].data_url);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { expect, beforeEach, afterEach, describe, it } from "vitest";
|
|
2
|
+
import { Database } from "../chat/data/database";
|
|
3
|
+
import { SessionMessage } from "../chat/data/dataModels";
|
|
4
|
+
import { ChatCompletionMessageParam } from "../agent/llm";
|
|
5
|
+
import {
|
|
6
|
+
TestUserSpec,
|
|
7
|
+
cleanupTestUser,
|
|
8
|
+
createTestSession,
|
|
9
|
+
getLocalDB,
|
|
10
|
+
testUserSpec,
|
|
11
|
+
} from "./dbTestTools";
|
|
12
|
+
import { DbSessionMessages } from "../chat/data/dbSessionMessages";
|
|
13
|
+
|
|
14
|
+
describe("DB Session Messages", () => {
|
|
15
|
+
let db: Database;
|
|
16
|
+
let dbsm: DbSessionMessages;
|
|
17
|
+
let testUsers: TestUserSpec[];
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Generate unique test data for each test
|
|
21
|
+
db = getLocalDB();
|
|
22
|
+
dbsm = db.createTypedClient(DbSessionMessages);
|
|
23
|
+
testUsers = [testUserSpec()];
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
await Promise.all(testUsers.map((tu) => cleanupTestUser(db, tu)));
|
|
28
|
+
testUsers = [];
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should update conversations", async () => {
|
|
32
|
+
const { sessionId } = await createTestSession(db, testUsers[0]);
|
|
33
|
+
|
|
34
|
+
const CONV_0: ChatCompletionMessageParam[] = [
|
|
35
|
+
{ role: "user", content: "message 0" },
|
|
36
|
+
{ role: "assistant", content: "message 1" },
|
|
37
|
+
];
|
|
38
|
+
const CONV_1: ChatCompletionMessageParam[] = [
|
|
39
|
+
{ role: "user", content: "message 2" },
|
|
40
|
+
{ role: "assistant", content: "message 3" },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const MESSGES_0: SessionMessage[] = [
|
|
44
|
+
{
|
|
45
|
+
message_idx: 0,
|
|
46
|
+
sender_uuid: testUsers[0].uuid,
|
|
47
|
+
is_for_llm: true,
|
|
48
|
+
content: CONV_0[0],
|
|
49
|
+
},
|
|
50
|
+
{ message_idx: 1, is_for_llm: true, content: CONV_0[1] },
|
|
51
|
+
];
|
|
52
|
+
const MESSGES_1: SessionMessage[] = [
|
|
53
|
+
{
|
|
54
|
+
message_idx: 2,
|
|
55
|
+
sender_uuid: testUsers[0].uuid,
|
|
56
|
+
is_for_llm: true,
|
|
57
|
+
content: CONV_1[0],
|
|
58
|
+
},
|
|
59
|
+
{ message_idx: 3, is_for_llm: true, content: CONV_1[1] },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Append messages to empty conversation
|
|
63
|
+
await dbsm.append(sessionId, MESSGES_0);
|
|
64
|
+
const msgs0 = await dbsm.getConversation(sessionId, 10);
|
|
65
|
+
expect(msgs0).eql(MESSGES_0);
|
|
66
|
+
|
|
67
|
+
// Append further messages
|
|
68
|
+
await dbsm.append(sessionId, MESSGES_1);
|
|
69
|
+
const msgs1 = await dbsm.getConversation(sessionId, 10);
|
|
70
|
+
expect(msgs1).eql(MESSGES_0.concat(MESSGES_1));
|
|
71
|
+
|
|
72
|
+
// Partial conversation (most recent 2)
|
|
73
|
+
const msgs2 = await dbsm.getConversation(sessionId, 2);
|
|
74
|
+
expect(msgs2).eql(MESSGES_1);
|
|
75
|
+
|
|
76
|
+
// Partial conversation (next 2, start at 2)
|
|
77
|
+
const msgs3 = await dbsm.getConversation(sessionId, 2, 2);
|
|
78
|
+
expect(msgs3).eql(MESSGES_0);
|
|
79
|
+
|
|
80
|
+
// Delete messages and check conv is empty
|
|
81
|
+
await dbsm.clearConversation(sessionId);
|
|
82
|
+
const msgs4 = await dbsm.getConversation(sessionId, 10);
|
|
83
|
+
expect(msgs4).eql([]);
|
|
84
|
+
});
|
|
85
|
+
});
|
package/src/test/dbTestTools.ts
CHANGED
|
@@ -78,7 +78,7 @@ export async function cleanupTestUser(
|
|
|
78
78
|
// api_key: string
|
|
79
79
|
): Promise<void> {
|
|
80
80
|
try {
|
|
81
|
-
await db.
|
|
81
|
+
await db.deleteUser(user.uuid);
|
|
82
82
|
} catch (_error) {
|
|
83
83
|
// Ignore cleanup errors
|
|
84
84
|
}
|
|
@@ -107,6 +107,7 @@ export async function createTestSession(
|
|
|
107
107
|
user_uuid: user1.uuid,
|
|
108
108
|
title: "test_session",
|
|
109
109
|
agent_profile_uuid: agentProfile.uuid,
|
|
110
|
+
agent_paused: false,
|
|
110
111
|
});
|
|
111
112
|
assert(sessionId);
|
|
112
113
|
|
|
@@ -122,11 +123,13 @@ export async function createTestTeamSession(
|
|
|
122
123
|
sessionId: string;
|
|
123
124
|
teamId: string;
|
|
124
125
|
}> {
|
|
125
|
-
|
|
126
|
-
createTestUser(db, testUsers[0]),
|
|
127
|
-
createTestUser(db, testUsers[1]),
|
|
128
|
-
]);
|
|
126
|
+
assert(testUsers.length > 0);
|
|
129
127
|
|
|
128
|
+
const [user1, _] = await Promise.all(
|
|
129
|
+
testUsers.map((tu) => createTestUser(db, tu))
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Profile for first user
|
|
130
133
|
const agentProfile = await db.createAgentProfile(
|
|
131
134
|
user1.uuid,
|
|
132
135
|
undefined,
|
|
@@ -146,6 +149,7 @@ export async function createTestTeamSession(
|
|
|
146
149
|
user_uuid: user1.uuid,
|
|
147
150
|
title: "test_session",
|
|
148
151
|
agent_profile_uuid: agentProfile.uuid,
|
|
152
|
+
agent_paused: false,
|
|
149
153
|
});
|
|
150
154
|
assert(sessionId);
|
|
151
155
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/require-await */
|
|
2
2
|
import { describe, it, expect } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import { loadImageAsDataUrlOrUndefined } from "../tool/files";
|
|
4
4
|
import { strict as assert } from "assert";
|
|
5
5
|
|
|
6
6
|
describe("Image loading", () => {
|
|
7
7
|
it("convert to data url", async () => {
|
|
8
|
-
const imageB64 =
|
|
8
|
+
const imageB64 = loadImageAsDataUrlOrUndefined("test_data/frog.png");
|
|
9
9
|
console.log(`imageB64: ${imageB64 || ""}`);
|
|
10
10
|
|
|
11
11
|
expect(imageB64).toBeTypeOf("string");
|
|
@@ -56,7 +56,9 @@ describe("McpServerManager", () => {
|
|
|
56
56
|
// Invoke
|
|
57
57
|
{
|
|
58
58
|
const qualifiedName = tm.getOpenAITools()[0].function.name;
|
|
59
|
-
const r = await tm.invoke(
|
|
59
|
+
const r = await tm.invoke(
|
|
60
|
+
tm.verifyToolCall(qualifiedName, { a: 1000, b: 10 })
|
|
61
|
+
);
|
|
60
62
|
console.log(`response: ${r}`);
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -122,4 +122,62 @@ describe("MultiAsyncQueue", () => {
|
|
|
122
122
|
expect(processedBatches).toHaveLength(2);
|
|
123
123
|
expect(processedBatches[1]).toEqual([2]);
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
it("should not process elements while paused", async () => {
|
|
127
|
+
// Place a 1 on the queue. Inside the process fn, place a 2 and 3 on the
|
|
128
|
+
// queue. Next invocation of process should see BOTH 2 AND 3.
|
|
129
|
+
|
|
130
|
+
const context = { q: undefined as MultiAsyncQueue<number> | undefined };
|
|
131
|
+
const seen: number[] = [];
|
|
132
|
+
let paused: number = 0;
|
|
133
|
+
const process = (elements: number[]): Promise<void> => {
|
|
134
|
+
console.log(`elements: ${JSON.stringify(elements)}`);
|
|
135
|
+
expect(context.q).toBeDefined();
|
|
136
|
+
|
|
137
|
+
for (const e of elements) {
|
|
138
|
+
seen.push(e);
|
|
139
|
+
}
|
|
140
|
+
if (seen.length === 1) {
|
|
141
|
+
context.q?.pause(); // Pause after first element
|
|
142
|
+
paused++;
|
|
143
|
+
expect(context.q?.tryEnqueue(2)).toBe(true);
|
|
144
|
+
expect(context.q?.tryEnqueue(3)).toBe(true);
|
|
145
|
+
} else if (seen.length === 3) {
|
|
146
|
+
expect(seen[1]).toBe(2);
|
|
147
|
+
expect(seen[2]).toBe(3);
|
|
148
|
+
} else {
|
|
149
|
+
throw new Error("unexpected elements");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return Promise.resolve();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
context.q = new MultiAsyncQueue<number>(process);
|
|
156
|
+
expect(context.q).toBeDefined();
|
|
157
|
+
expect(context.q.tryEnqueue(1)).toBe(true);
|
|
158
|
+
|
|
159
|
+
await new Promise<void>((r) => {
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
expect(seen).toEqual([1]);
|
|
162
|
+
r();
|
|
163
|
+
}, 100);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(context.q).toBeDefined();
|
|
167
|
+
expect(seen).toEqual([1]);
|
|
168
|
+
expect(paused).toEqual(1);
|
|
169
|
+
|
|
170
|
+
context.q.unpause();
|
|
171
|
+
|
|
172
|
+
await new Promise<void>((r) => {
|
|
173
|
+
setTimeout(() => {
|
|
174
|
+
expect(seen).toEqual([1, 2, 3]);
|
|
175
|
+
r();
|
|
176
|
+
}, 100);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(context.q).toBeDefined();
|
|
180
|
+
expect(seen).toEqual([1, 2, 3]);
|
|
181
|
+
expect(paused).toEqual(1);
|
|
182
|
+
});
|
|
125
183
|
});
|
package/src/test/testTools.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
ChatCompletionMessageParam,
|
|
6
6
|
ChatCompletionMessageToolCall,
|
|
7
7
|
ChatCompletionToolMessageParam,
|
|
8
|
-
} from "../agent/
|
|
8
|
+
} from "../agent/llm";
|
|
9
9
|
import { IAgentEventHandler } from "../agent/iAgentEventHandler";
|
|
10
10
|
import { IPlatform } from "../agent/iplatform";
|
|
11
11
|
import { DummyLLM } from "../agent/dummyLLM";
|
|
@@ -29,7 +29,9 @@ export const DUMMY_PLATFORM: IPlatform = {
|
|
|
29
29
|
export class TestAgentEventHandler implements IAgentEventHandler {
|
|
30
30
|
private all: ChatCompletionMessageParam[] = [];
|
|
31
31
|
private completions: ChatCompletionAssistantMessageParam[] = [];
|
|
32
|
+
private images: OpenAI.Chat.Completions.ChatCompletionContentPartImage[] = [];
|
|
32
33
|
private agentMessages: string[] = [""];
|
|
34
|
+
private reasoning: string[] = [""];
|
|
33
35
|
private toolCalls: ChatCompletionMessageToolCall[] = [];
|
|
34
36
|
private agentToolCalls: ChatCompletionMessageToolCall[] = [];
|
|
35
37
|
private toolCallResults: OpenAI.ChatCompletionToolMessageParam[] = [];
|
|
@@ -39,6 +41,10 @@ export class TestAgentEventHandler implements IAgentEventHandler {
|
|
|
39
41
|
this.all.push(result);
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
onImage(image: OpenAI.Chat.Completions.ChatCompletionContentPartImage) {
|
|
45
|
+
this.images.push(image);
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
onToolCallResult(result: ChatCompletionToolMessageParam): void {
|
|
43
49
|
this.toolCallResults.push(result);
|
|
44
50
|
this.all.push(result);
|
|
@@ -48,12 +54,20 @@ export class TestAgentEventHandler implements IAgentEventHandler {
|
|
|
48
54
|
this.agentMessages[this.agentMessages.length - 1] += chunk;
|
|
49
55
|
if (isEnd) {
|
|
50
56
|
this.agentMessages.push("");
|
|
57
|
+
this.reasoning.push("");
|
|
51
58
|
}
|
|
52
59
|
return new Promise<void>((r) => {
|
|
53
60
|
r();
|
|
54
61
|
});
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
onReasoning(reasoning: string): Promise<void> {
|
|
65
|
+
this.reasoning[this.reasoning.length - 1] += reasoning;
|
|
66
|
+
return new Promise<void>((r) => {
|
|
67
|
+
r();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
onToolCall(
|
|
58
72
|
toolCall: ChatCompletionMessageToolCall,
|
|
59
73
|
isAgentTool: boolean
|
package/src/tool/agentChat.ts
CHANGED
|
@@ -10,14 +10,18 @@ import { AgentProfile } from "../agent/agent";
|
|
|
10
10
|
import { createAgentWithSkills } from "../agent/agentUtils";
|
|
11
11
|
import { IAgentEventHandler } from "../agent/iAgentEventHandler";
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { loadImageAsDataUrlOrUndefined } from "./files";
|
|
14
14
|
import { NODE_PLATFORM } from "./nodePlatform";
|
|
15
15
|
import { CommandPrompt } from "./commandPrompt";
|
|
16
16
|
import { IPrompt, Prompt } from "./prompt";
|
|
17
17
|
import { ContextManager } from "../agent/context";
|
|
18
|
+
import { ChatCompletionMessageParam } from "../agent/llm";
|
|
18
19
|
|
|
19
20
|
const logger = getLogger();
|
|
20
21
|
|
|
22
|
+
export const DEFAULT_AGENT_LLM_MODEL =
|
|
23
|
+
process.env["DEFAULT_LLM_MODEL"] || "anthropic/claude-sonnet-4.5";
|
|
24
|
+
|
|
21
25
|
async function write(msg: string): Promise<void> {
|
|
22
26
|
return new Promise((resolve, err) => {
|
|
23
27
|
process.stdout.write(msg, (e) => {
|
|
@@ -33,7 +37,7 @@ async function write(msg: string): Promise<void> {
|
|
|
33
37
|
export async function runChat(
|
|
34
38
|
llmUrl: string,
|
|
35
39
|
agentProfile: AgentProfile,
|
|
36
|
-
conversation:
|
|
40
|
+
conversation: ChatCompletionMessageParam[] | undefined,
|
|
37
41
|
prompt: string | undefined,
|
|
38
42
|
image: string | undefined,
|
|
39
43
|
llmApiKey: string | undefined,
|
|
@@ -46,15 +50,18 @@ export async function runChat(
|
|
|
46
50
|
|
|
47
51
|
const spinner: Spinner = yocto();
|
|
48
52
|
let first = true;
|
|
53
|
+
let reasoningFirst = true;
|
|
49
54
|
|
|
50
55
|
const onAgentMessage = async (msg: string, msgEnd: boolean) => {
|
|
51
56
|
if (first) {
|
|
52
57
|
first = false;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
spinner.stop().clear();
|
|
58
|
+
|
|
59
|
+
if (!reasoningFirst) {
|
|
60
|
+
await write(`${chalk.grey("]")}\n`);
|
|
57
61
|
}
|
|
62
|
+
|
|
63
|
+
await write("AGENT: ");
|
|
64
|
+
spinner.stop().clear();
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
if (msg) {
|
|
@@ -64,9 +71,20 @@ export async function runChat(
|
|
|
64
71
|
if (msgEnd) {
|
|
65
72
|
await write("\n");
|
|
66
73
|
first = true;
|
|
74
|
+
reasoningFirst = true;
|
|
67
75
|
}
|
|
68
76
|
};
|
|
69
77
|
|
|
78
|
+
const onReasoning = async (reasoning: string) => {
|
|
79
|
+
logger.debug(`[AgentChat.onReasoning]: ${reasoning}`);
|
|
80
|
+
if (reasoningFirst) {
|
|
81
|
+
spinner.stop().clear();
|
|
82
|
+
await write(chalk.grey("[REASONING: "));
|
|
83
|
+
reasoningFirst = false;
|
|
84
|
+
}
|
|
85
|
+
await write(chalk.grey(reasoning));
|
|
86
|
+
};
|
|
87
|
+
|
|
70
88
|
const repl: IPrompt = new Prompt();
|
|
71
89
|
const cmdPrompt = new CommandPrompt(repl);
|
|
72
90
|
|
|
@@ -88,10 +106,19 @@ export async function runChat(
|
|
|
88
106
|
logger.debug(`Tool call result: ${JSON.stringify(result)}`);
|
|
89
107
|
};
|
|
90
108
|
|
|
109
|
+
const onImage = (
|
|
110
|
+
image: OpenAI.Chat.Completions.ChatCompletionContentPartImage
|
|
111
|
+
) => {
|
|
112
|
+
const dataUrl = image.image_url.url;
|
|
113
|
+
void NODE_PLATFORM.renderHTML(`<img src="${dataUrl}" />`);
|
|
114
|
+
};
|
|
115
|
+
|
|
91
116
|
// Create event handler for CLI agent
|
|
92
117
|
const eventHandler: IAgentEventHandler = {
|
|
93
118
|
onCompletion: () => {},
|
|
119
|
+
onImage,
|
|
94
120
|
onAgentMessage,
|
|
121
|
+
onReasoning,
|
|
95
122
|
onToolCall,
|
|
96
123
|
onToolCallResult,
|
|
97
124
|
};
|
|
@@ -101,6 +128,7 @@ export async function runChat(
|
|
|
101
128
|
const [agent, sudoMcpServerManager] = await createAgentWithSkills(
|
|
102
129
|
llmUrl,
|
|
103
130
|
agentProfile,
|
|
131
|
+
DEFAULT_AGENT_LLM_MODEL,
|
|
104
132
|
eventHandler,
|
|
105
133
|
NODE_PLATFORM,
|
|
106
134
|
new ContextManager(agentProfile.system_prompt, conversation || []),
|
|
@@ -136,7 +164,7 @@ export async function runChat(
|
|
|
136
164
|
image = img;
|
|
137
165
|
}
|
|
138
166
|
|
|
139
|
-
image =
|
|
167
|
+
image = loadImageAsDataUrlOrUndefined(image);
|
|
140
168
|
|
|
141
169
|
// Pass the prompt and image to the Agent
|
|
142
170
|
try {
|
package/src/tool/agentMain.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as dotenv from "dotenv";
|
|
3
3
|
import { command, option, flag, optional, string } from "cmd-ts";
|
|
4
|
-
import OpenAI from "openai";
|
|
5
4
|
import { strict as assert } from "assert";
|
|
6
5
|
|
|
7
6
|
import { configuration, utils } from "@xalia/xmcp/tool";
|
|
@@ -22,10 +21,11 @@ import {
|
|
|
22
21
|
import {
|
|
23
22
|
loadFileOrUndefined,
|
|
24
23
|
loadFileOrStdin,
|
|
25
|
-
|
|
24
|
+
loadImageAsDataUrlOrUndefined,
|
|
26
25
|
} from "./files";
|
|
27
|
-
import { runChat } from "./agentChat";
|
|
26
|
+
import { DEFAULT_AGENT_LLM_MODEL, runChat } from "./agentChat";
|
|
28
27
|
import { NODE_PLATFORM } from "./nodePlatform";
|
|
28
|
+
import { ChatCompletionMessageParam } from "../agent/llm";
|
|
29
29
|
|
|
30
30
|
dotenv.config();
|
|
31
31
|
|
|
@@ -128,9 +128,8 @@ export const agentMain = command({
|
|
|
128
128
|
|
|
129
129
|
// Restore conversation from value or file.
|
|
130
130
|
|
|
131
|
-
const startingConversation:
|
|
132
|
-
|
|
133
|
-
| undefined = utils.loadContentOrFileOrUndefined(conversationFile);
|
|
131
|
+
const startingConversation: ChatCompletionMessageParam[] | undefined =
|
|
132
|
+
utils.loadContentOrFileOrUndefined(conversationFile);
|
|
134
133
|
logger.debug(
|
|
135
134
|
`startingConversation: ${JSON.stringify(startingConversation)}`
|
|
136
135
|
);
|
|
@@ -146,10 +145,11 @@ export const agentMain = command({
|
|
|
146
145
|
const { response, conversation } = await runOneShot(
|
|
147
146
|
llmUrl,
|
|
148
147
|
agentProfile,
|
|
148
|
+
DEFAULT_AGENT_LLM_MODEL,
|
|
149
149
|
startingConversation,
|
|
150
150
|
NODE_PLATFORM,
|
|
151
151
|
prompt,
|
|
152
|
-
|
|
152
|
+
loadImageAsDataUrlOrUndefined(imageFile),
|
|
153
153
|
llmApiKey,
|
|
154
154
|
sudomcpConfig,
|
|
155
155
|
approveToolsUpTo
|