@xalia/agent 0.5.7 → 0.6.0
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/README.md +23 -8
- package/dist/agent/src/agent/agent.js +176 -96
- package/dist/agent/src/agent/agentUtils.js +82 -59
- package/dist/agent/src/agent/compressingContextManager.js +102 -0
- package/dist/agent/src/agent/context.js +189 -0
- package/dist/agent/src/agent/dummyLLM.js +46 -5
- package/dist/agent/src/agent/mcpServerManager.js +23 -24
- package/dist/agent/src/agent/nullAgentEventHandler.js +21 -0
- package/dist/agent/src/agent/nullPlatform.js +14 -0
- package/dist/agent/src/agent/openAILLMStreaming.js +26 -14
- package/dist/agent/src/agent/promptProvider.js +63 -0
- package/dist/agent/src/agent/repeatLLM.js +5 -5
- package/dist/agent/src/agent/sudoMcpServerManager.js +23 -21
- package/dist/agent/src/agent/tokenAuth.js +7 -7
- package/dist/agent/src/agent/tools.js +1 -1
- package/dist/agent/src/chat/client/chatClient.js +733 -0
- package/dist/agent/src/chat/client/connection.js +209 -0
- package/dist/agent/src/chat/client/connection.test.js +188 -0
- package/dist/agent/src/chat/client/constants.js +5 -0
- package/dist/agent/src/chat/client/index.js +15 -0
- package/dist/agent/src/chat/client/interfaces.js +2 -0
- package/dist/agent/src/chat/client/responseHandler.js +105 -0
- package/dist/agent/src/chat/client/sessionClient.js +331 -0
- package/dist/agent/src/chat/client/teamManager.js +2 -0
- package/dist/agent/src/chat/{apiKeyManager.js → data/apiKeyManager.js} +4 -0
- package/dist/agent/src/chat/data/dataModels.js +2 -0
- package/dist/agent/src/chat/data/database.js +749 -0
- package/dist/agent/src/chat/data/dbMcpServerConfigs.js +47 -0
- package/dist/agent/src/chat/protocol/connectionMessages.js +5 -0
- package/dist/agent/src/chat/protocol/constants.js +50 -0
- package/dist/agent/src/chat/protocol/errors.js +22 -0
- package/dist/agent/src/chat/protocol/messages.js +110 -0
- package/dist/agent/src/chat/server/chatContextManager.js +405 -0
- package/dist/agent/src/chat/server/connectionManager.js +352 -0
- package/dist/agent/src/chat/server/connectionManager.test.js +159 -0
- package/dist/agent/src/chat/server/conversation.js +198 -0
- package/dist/agent/src/chat/server/errorUtils.js +23 -0
- package/dist/agent/src/chat/server/openSession.js +869 -0
- package/dist/agent/src/chat/server/server.js +177 -0
- package/dist/agent/src/chat/server/sessionFileManager.js +161 -0
- package/dist/agent/src/chat/server/sessionRegistry.js +700 -0
- package/dist/agent/src/chat/server/sessionRegistry.test.js +97 -0
- package/dist/agent/src/chat/server/test-utils/mockFactories.js +307 -0
- package/dist/agent/src/chat/server/tools.js +243 -0
- package/dist/agent/src/chat/utils/agentSessionMap.js +66 -0
- package/dist/agent/src/chat/utils/approvalManager.js +85 -0
- package/dist/agent/src/{utils → chat/utils}/asyncLock.js +3 -3
- package/dist/agent/src/chat/{asyncQueue.js → utils/asyncQueue.js} +12 -2
- package/dist/agent/src/chat/utils/htmlToText.js +84 -0
- package/dist/agent/src/chat/utils/multiAsyncQueue.js +42 -0
- package/dist/agent/src/chat/utils/search.js +145 -0
- package/dist/agent/src/chat/utils/userResolver.js +46 -0
- package/dist/agent/src/chat/utils/websocket.js +16 -0
- package/dist/agent/src/test/agent.test.js +332 -0
- package/dist/agent/src/test/approvalManager.test.js +58 -0
- package/dist/agent/src/test/chatContextManager.test.js +392 -0
- package/dist/agent/src/test/clientServerConnection.test.js +158 -0
- package/dist/agent/src/test/compressingContextManager.test.js +65 -0
- package/dist/agent/src/test/context.test.js +83 -0
- package/dist/agent/src/test/conversation.test.js +89 -0
- package/dist/agent/src/test/db.test.js +271 -83
- package/dist/agent/src/test/dbMcpServerConfigs.test.js +72 -0
- package/dist/agent/src/test/dbTestTools.js +99 -0
- package/dist/agent/src/test/imageLoad.test.js +8 -7
- package/dist/agent/src/test/mcpServerManager.test.js +23 -20
- package/dist/agent/src/test/multiAsyncQueue.test.js +101 -0
- package/dist/agent/src/test/openaiStreaming.test.js +64 -35
- package/dist/agent/src/test/prompt.test.js +5 -4
- package/dist/agent/src/test/promptProvider.test.js +28 -0
- package/dist/agent/src/test/responseHandler.test.js +61 -0
- package/dist/agent/src/test/sudoMcpServerManager.test.js +24 -25
- package/dist/agent/src/test/testTools.js +109 -0
- package/dist/agent/src/test/tools.test.js +31 -0
- package/dist/agent/src/tool/agentChat.js +21 -10
- package/dist/agent/src/tool/agentMain.js +1 -1
- package/dist/agent/src/tool/chatMain.js +241 -58
- package/dist/agent/src/tool/commandPrompt.js +22 -17
- package/dist/agent/src/tool/files.js +20 -16
- package/dist/agent/src/tool/nodePlatform.js +47 -3
- package/dist/agent/src/tool/options.js +4 -4
- package/dist/agent/src/tool/prompt.js +19 -13
- package/eslint.config.mjs +14 -1
- package/package.json +14 -6
- package/scripts/chat_server +8 -0
- package/scripts/setup_chat +7 -2
- package/scripts/shutdown_chat_server +3 -0
- package/scripts/test_chat +135 -17
- package/src/agent/agent.ts +283 -138
- package/src/agent/agentUtils.ts +143 -108
- package/src/agent/compressingContextManager.ts +164 -0
- package/src/agent/context.ts +268 -0
- package/src/agent/dummyLLM.ts +76 -8
- package/src/agent/iAgentEventHandler.ts +54 -0
- package/src/agent/iplatform.ts +1 -0
- package/src/agent/mcpServerManager.ts +35 -31
- package/src/agent/nullAgentEventHandler.ts +20 -0
- package/src/agent/nullPlatform.ts +13 -0
- package/src/agent/openAILLMStreaming.ts +26 -13
- package/src/agent/promptProvider.ts +87 -0
- package/src/agent/repeatLLM.ts +5 -5
- package/src/agent/sudoMcpServerManager.ts +30 -29
- package/src/agent/tokenAuth.ts +7 -7
- package/src/agent/tools.ts +3 -1
- package/src/chat/client/chatClient.ts +900 -0
- package/src/chat/client/connection.test.ts +241 -0
- package/src/chat/client/connection.ts +276 -0
- package/src/chat/client/constants.ts +3 -0
- package/src/chat/client/index.ts +18 -0
- package/src/chat/client/interfaces.ts +34 -0
- package/src/chat/client/responseHandler.ts +131 -0
- package/src/chat/client/sessionClient.ts +443 -0
- package/src/chat/client/teamManager.ts +29 -0
- package/src/chat/{apiKeyManager.ts → data/apiKeyManager.ts} +6 -2
- package/src/chat/data/dataModels.ts +85 -0
- package/src/chat/data/database.ts +982 -0
- package/src/chat/data/dbMcpServerConfigs.ts +59 -0
- package/src/chat/protocol/connectionMessages.ts +49 -0
- package/src/chat/protocol/constants.ts +55 -0
- package/src/chat/protocol/errors.ts +16 -0
- package/src/chat/protocol/messages.ts +682 -0
- package/src/chat/server/README.md +127 -0
- package/src/chat/server/chatContextManager.ts +612 -0
- package/src/chat/server/connectionManager.test.ts +266 -0
- package/src/chat/server/connectionManager.ts +541 -0
- package/src/chat/server/conversation.ts +269 -0
- package/src/chat/server/errorUtils.ts +28 -0
- package/src/chat/server/openSession.ts +1332 -0
- package/src/chat/server/server.ts +177 -0
- package/src/chat/server/sessionFileManager.ts +239 -0
- package/src/chat/server/sessionRegistry.test.ts +138 -0
- package/src/chat/server/sessionRegistry.ts +1064 -0
- package/src/chat/server/test-utils/mockFactories.ts +422 -0
- package/src/chat/server/tools.ts +265 -0
- package/src/chat/utils/agentSessionMap.ts +76 -0
- package/src/chat/utils/approvalManager.ts +111 -0
- package/src/{utils → chat/utils}/asyncLock.ts +3 -3
- package/src/chat/{asyncQueue.ts → utils/asyncQueue.ts} +14 -3
- package/src/chat/utils/htmlToText.ts +61 -0
- package/src/chat/utils/multiAsyncQueue.ts +52 -0
- package/src/chat/utils/search.ts +139 -0
- package/src/chat/utils/userResolver.ts +48 -0
- package/src/chat/utils/websocket.ts +16 -0
- package/src/test/agent.test.ts +487 -0
- package/src/test/approvalManager.test.ts +73 -0
- package/src/test/chatContextManager.test.ts +521 -0
- package/src/test/clientServerConnection.test.ts +207 -0
- package/src/test/compressingContextManager.test.ts +82 -0
- package/src/test/context.test.ts +105 -0
- package/src/test/conversation.test.ts +109 -0
- package/src/test/db.test.ts +358 -89
- package/src/test/dbMcpServerConfigs.test.ts +112 -0
- package/src/test/dbTestTools.ts +153 -0
- package/src/test/imageLoad.test.ts +7 -6
- package/src/test/mcpServerManager.test.ts +21 -16
- package/src/test/multiAsyncQueue.test.ts +125 -0
- package/src/test/openaiStreaming.test.ts +71 -36
- package/src/test/prompt.test.ts +4 -3
- package/src/test/promptProvider.test.ts +33 -0
- package/src/test/responseHandler.test.ts +78 -0
- package/src/test/sudoMcpServerManager.test.ts +32 -30
- package/src/test/testTools.ts +146 -0
- package/src/test/tools.test.ts +39 -0
- package/src/tool/agentChat.ts +26 -12
- package/src/tool/agentMain.ts +1 -1
- package/src/tool/chatMain.ts +292 -100
- package/src/tool/commandPrompt.ts +28 -19
- package/src/tool/files.ts +25 -19
- package/src/tool/nodePlatform.ts +52 -3
- package/src/tool/options.ts +4 -2
- package/src/tool/prompt.ts +22 -15
- package/test_data/dummyllm_script_crash.json +32 -0
- package/test_data/frog.png.b64 +1 -0
- package/vitest.config.ts +39 -0
- package/dist/agent/src/chat/client.js +0 -349
- package/dist/agent/src/chat/conversationManager.js +0 -392
- package/dist/agent/src/chat/db.js +0 -209
- package/dist/agent/src/chat/frontendClient.js +0 -74
- package/dist/agent/src/chat/server.js +0 -158
- package/src/chat/client.ts +0 -455
- package/src/chat/conversationManager.ts +0 -595
- package/src/chat/db.ts +0 -290
- package/src/chat/frontendClient.ts +0 -123
- package/src/chat/messages.ts +0 -235
- package/src/chat/server.ts +0 -177
- /package/dist/agent/src/{chat/messages.js → agent/iAgentEventHandler.js} +0 -0
- /package/{frog.png → test_data/frog.png} +0 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common mock factories for chat server tests
|
|
3
|
+
* Provides consistent mock objects across all test suites
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { vi } from "vitest";
|
|
7
|
+
import type { Database, UserData } from "../../data/database";
|
|
8
|
+
import type { SessionData } from "../../data/dataModels";
|
|
9
|
+
import type { ApiKeyManager } from "../../data/apiKeyManager";
|
|
10
|
+
import type { OpenSession } from "../openSession";
|
|
11
|
+
import type { WebSocket } from "ws";
|
|
12
|
+
import type { AgentProfile } from "@xalia/xmcp/sdk";
|
|
13
|
+
import type * as supabase from "../../../../../supabase/database.types";
|
|
14
|
+
import { ClientToServer, ServerToClient } from "../../protocol/messages";
|
|
15
|
+
import {
|
|
16
|
+
IMessageProcessor,
|
|
17
|
+
IUserConnectionManager,
|
|
18
|
+
} from "../connectionManager";
|
|
19
|
+
|
|
20
|
+
// =====================================
|
|
21
|
+
// Mock Data Constants
|
|
22
|
+
// =====================================
|
|
23
|
+
|
|
24
|
+
export const MOCK_USERS = {
|
|
25
|
+
owner: {
|
|
26
|
+
uuid: "user-123",
|
|
27
|
+
nickname: "test-owner",
|
|
28
|
+
} as UserData,
|
|
29
|
+
participant: {
|
|
30
|
+
uuid: "user-456",
|
|
31
|
+
nickname: "test-participant",
|
|
32
|
+
} as UserData,
|
|
33
|
+
nonExistent: {
|
|
34
|
+
uuid: "user-999",
|
|
35
|
+
nickname: "non-existent",
|
|
36
|
+
} as UserData,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const MOCK_SESSIONS = {
|
|
40
|
+
active: {
|
|
41
|
+
uuid: "session-abc",
|
|
42
|
+
title: "Test Session",
|
|
43
|
+
user_uuid: MOCK_USERS.owner.uuid,
|
|
44
|
+
agent_profile_uuid: "agent-profile-1",
|
|
45
|
+
workspace: null,
|
|
46
|
+
team_uuid: null,
|
|
47
|
+
public: false,
|
|
48
|
+
updated_at: "2025-01-01T00:00:00Z",
|
|
49
|
+
} as supabase.Tables<"sessions">,
|
|
50
|
+
secondary: {
|
|
51
|
+
uuid: "session-xyz",
|
|
52
|
+
title: "Secondary Session",
|
|
53
|
+
user_uuid: MOCK_USERS.owner.uuid,
|
|
54
|
+
agent_profile_uuid: "agent-profile-2",
|
|
55
|
+
workspace: null,
|
|
56
|
+
team_uuid: null,
|
|
57
|
+
public: false,
|
|
58
|
+
updated_at: "2025-01-01T01:00:00Z",
|
|
59
|
+
} as supabase.Tables<"sessions">,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const MOCK_AGENT_PROFILES = {
|
|
63
|
+
default: {
|
|
64
|
+
system_prompt: "You are a helpful assistant",
|
|
65
|
+
model: "gpt-4",
|
|
66
|
+
mcp_settings: {},
|
|
67
|
+
} as AgentProfile,
|
|
68
|
+
custom: {
|
|
69
|
+
system_prompt: "You are a specialized assistant",
|
|
70
|
+
model: "claude-3",
|
|
71
|
+
mcp_settings: {
|
|
72
|
+
filesystem: ["read_file", "write_file"],
|
|
73
|
+
},
|
|
74
|
+
} as AgentProfile,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// =====================================
|
|
78
|
+
// Mock Factory Functions
|
|
79
|
+
// =====================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a mock Database instance with common methods
|
|
83
|
+
*/
|
|
84
|
+
export function createMockDatabase(): {
|
|
85
|
+
mock: Database;
|
|
86
|
+
spies: {
|
|
87
|
+
sessionCreate: ReturnType<typeof vi.fn>;
|
|
88
|
+
sessionGetById: ReturnType<typeof vi.fn>;
|
|
89
|
+
getUserFromUuid: ReturnType<typeof vi.fn>;
|
|
90
|
+
getAgentProfileById: ReturnType<typeof vi.fn>;
|
|
91
|
+
getSavedAgentProfileById: ReturnType<typeof vi.fn>;
|
|
92
|
+
getUserApiKey: ReturnType<typeof vi.fn>;
|
|
93
|
+
sessionParticipantAdd: ReturnType<typeof vi.fn>;
|
|
94
|
+
sessionParticipantRemove: ReturnType<typeof vi.fn>;
|
|
95
|
+
updateAgentProfile: ReturnType<typeof vi.fn>;
|
|
96
|
+
sessionConversationAppend: ReturnType<typeof vi.fn>;
|
|
97
|
+
sessionGetParticipants: ReturnType<typeof vi.fn>;
|
|
98
|
+
getUserSessions: ReturnType<typeof vi.fn>;
|
|
99
|
+
};
|
|
100
|
+
} {
|
|
101
|
+
const spies = {
|
|
102
|
+
sessionCreate: vi.fn(),
|
|
103
|
+
sessionGetById: vi.fn(),
|
|
104
|
+
getUserFromUuid: vi.fn(),
|
|
105
|
+
getAgentProfileById: vi.fn(),
|
|
106
|
+
getSavedAgentProfileById: vi.fn(),
|
|
107
|
+
getUserApiKey: vi.fn(),
|
|
108
|
+
sessionParticipantAdd: vi.fn(),
|
|
109
|
+
sessionParticipantRemove: vi.fn(),
|
|
110
|
+
updateAgentProfile: vi.fn(),
|
|
111
|
+
sessionConversationAppend: vi.fn(),
|
|
112
|
+
sessionGetParticipants: vi.fn(),
|
|
113
|
+
getUserSessions: vi.fn(),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const mock = {
|
|
117
|
+
...spies,
|
|
118
|
+
// Add any other Database methods that might be called
|
|
119
|
+
} as unknown as Database;
|
|
120
|
+
|
|
121
|
+
return { mock, spies };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Creates a mock IUserConnectionManager instance
|
|
126
|
+
*/
|
|
127
|
+
export function createMockUserConnectionManager(): {
|
|
128
|
+
mock: IUserConnectionManager<ServerToClient>;
|
|
129
|
+
spies: {
|
|
130
|
+
sendToUsers: ReturnType<typeof vi.fn>;
|
|
131
|
+
getLiveUserApiKey: ReturnType<typeof vi.fn>;
|
|
132
|
+
};
|
|
133
|
+
} {
|
|
134
|
+
const spies = {
|
|
135
|
+
sendToUsers: vi.fn(),
|
|
136
|
+
sendToConnection: vi.fn(),
|
|
137
|
+
sendServerError: vi.fn(),
|
|
138
|
+
getLiveUserApiKey: vi.fn(),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const mock = {
|
|
142
|
+
sendToUsers: spies.sendToUsers,
|
|
143
|
+
sendToConnection: spies.sendToConnection,
|
|
144
|
+
sendServerError: spies.sendServerError,
|
|
145
|
+
getLiveUserApiKey: spies.getLiveUserApiKey,
|
|
146
|
+
} as IUserConnectionManager<ServerToClient>;
|
|
147
|
+
|
|
148
|
+
return { mock, spies };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Creates a mock IMessageProcessor instance
|
|
153
|
+
*/
|
|
154
|
+
export function createMockSessionRegistry(): {
|
|
155
|
+
mock: IMessageProcessor<ClientToServer>;
|
|
156
|
+
spies: {
|
|
157
|
+
processMessage: ReturnType<typeof vi.fn>;
|
|
158
|
+
handleUserDisconnect: ReturnType<typeof vi.fn>;
|
|
159
|
+
};
|
|
160
|
+
} {
|
|
161
|
+
const spies = {
|
|
162
|
+
processMessage: vi.fn(),
|
|
163
|
+
handleUserDisconnect: vi.fn(),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const mock = {
|
|
167
|
+
processMessage: spies.processMessage,
|
|
168
|
+
handleUserDisconnect: spies.handleUserDisconnect,
|
|
169
|
+
} as IMessageProcessor<ClientToServer>;
|
|
170
|
+
|
|
171
|
+
return { mock, spies };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Creates a mock ApiKeyManager instance
|
|
176
|
+
*/
|
|
177
|
+
export function createMockApiKeyManager(): {
|
|
178
|
+
mock: ApiKeyManager;
|
|
179
|
+
spies: {
|
|
180
|
+
verifyApiKey: ReturnType<typeof vi.fn>;
|
|
181
|
+
};
|
|
182
|
+
} {
|
|
183
|
+
const spies = {
|
|
184
|
+
verifyApiKey: vi.fn(),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const mock = {
|
|
188
|
+
...spies,
|
|
189
|
+
} as unknown as ApiKeyManager;
|
|
190
|
+
|
|
191
|
+
return { mock, spies };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Creates a mock WebSocket instance
|
|
196
|
+
*/
|
|
197
|
+
export function createMockWebSocket(readyState: number = 1): {
|
|
198
|
+
mock: WebSocket;
|
|
199
|
+
spies: {
|
|
200
|
+
on: ReturnType<typeof vi.fn>;
|
|
201
|
+
once: ReturnType<typeof vi.fn>;
|
|
202
|
+
send: ReturnType<typeof vi.fn>;
|
|
203
|
+
close: ReturnType<typeof vi.fn>;
|
|
204
|
+
};
|
|
205
|
+
} {
|
|
206
|
+
const spies = {
|
|
207
|
+
on: vi.fn(),
|
|
208
|
+
once: vi.fn(),
|
|
209
|
+
send: vi.fn(),
|
|
210
|
+
close: vi.fn(),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const mock = {
|
|
214
|
+
...spies,
|
|
215
|
+
readyState, // Allow setting initial readyState
|
|
216
|
+
} as unknown as WebSocket;
|
|
217
|
+
|
|
218
|
+
return { mock, spies };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Creates a mock OpenSession instance
|
|
223
|
+
*/
|
|
224
|
+
export function createMockOpenSession(sessionId = MOCK_SESSIONS.active.uuid): {
|
|
225
|
+
mock: OpenSession;
|
|
226
|
+
spies: {
|
|
227
|
+
addParticipant: ReturnType<typeof vi.fn>;
|
|
228
|
+
leave: ReturnType<typeof vi.fn>;
|
|
229
|
+
onClientMessage: ReturnType<typeof vi.fn>;
|
|
230
|
+
getSessionState: ReturnType<typeof vi.fn>;
|
|
231
|
+
broadcast: ReturnType<typeof vi.fn>;
|
|
232
|
+
sendTo: ReturnType<typeof vi.fn>;
|
|
233
|
+
onEmpty: ReturnType<typeof vi.fn>;
|
|
234
|
+
};
|
|
235
|
+
} {
|
|
236
|
+
const spies = {
|
|
237
|
+
addParticipant: vi.fn(),
|
|
238
|
+
leave: vi.fn(),
|
|
239
|
+
onClientMessage: vi.fn(),
|
|
240
|
+
getSessionState: vi.fn(),
|
|
241
|
+
broadcast: vi.fn(),
|
|
242
|
+
sendTo: vi.fn(),
|
|
243
|
+
onEmpty: vi.fn(),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const mock = {
|
|
247
|
+
...spies,
|
|
248
|
+
sessionUUID: sessionId,
|
|
249
|
+
users: new Set<string>(),
|
|
250
|
+
agentProfileUUID: MOCK_SESSIONS.active.agent_profile_uuid,
|
|
251
|
+
sessionOwnerUserUUID: MOCK_USERS.owner.uuid,
|
|
252
|
+
} as unknown as OpenSession;
|
|
253
|
+
|
|
254
|
+
return { mock, spies };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Creates a standard set of session list items for testing
|
|
259
|
+
*/
|
|
260
|
+
export function createMockSessionList(): Array<SessionData> {
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
session_uuid: MOCK_SESSIONS.active.uuid,
|
|
264
|
+
title: MOCK_SESSIONS.active.title,
|
|
265
|
+
team_uuid: undefined,
|
|
266
|
+
agent_profile_uuid: MOCK_SESSIONS.active.agent_profile_uuid,
|
|
267
|
+
workspace: undefined,
|
|
268
|
+
updated_at: MOCK_SESSIONS.active.updated_at || "",
|
|
269
|
+
user_uuid: MOCK_SESSIONS.active.user_uuid,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
session_uuid: MOCK_SESSIONS.secondary.uuid,
|
|
273
|
+
title: MOCK_SESSIONS.secondary.title,
|
|
274
|
+
team_uuid: undefined,
|
|
275
|
+
agent_profile_uuid: MOCK_SESSIONS.secondary.agent_profile_uuid,
|
|
276
|
+
workspace: undefined,
|
|
277
|
+
updated_at: MOCK_SESSIONS.secondary.updated_at || "",
|
|
278
|
+
user_uuid: MOCK_SESSIONS.secondary.user_uuid,
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Helper to setup common mock behaviors
|
|
285
|
+
*/
|
|
286
|
+
export function setupStandardMockBehaviors(mocks: {
|
|
287
|
+
database?: ReturnType<typeof createMockDatabase>;
|
|
288
|
+
apiKeyManager?: ReturnType<typeof createMockApiKeyManager>;
|
|
289
|
+
userConnectionManager?: ReturnType<typeof createMockUserConnectionManager>;
|
|
290
|
+
}) {
|
|
291
|
+
// Setup database mocks
|
|
292
|
+
if (mocks.database) {
|
|
293
|
+
mocks.database.spies.getUserFromUuid.mockImplementation(
|
|
294
|
+
(userId: string) => {
|
|
295
|
+
if (userId === MOCK_USERS.owner.uuid)
|
|
296
|
+
return Promise.resolve(MOCK_USERS.owner);
|
|
297
|
+
if (userId === MOCK_USERS.participant.uuid)
|
|
298
|
+
return Promise.resolve(MOCK_USERS.participant);
|
|
299
|
+
return Promise.resolve(null);
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
mocks.database.spies.sessionGetById.mockImplementation(
|
|
304
|
+
(sessionId: string) => {
|
|
305
|
+
if (sessionId === MOCK_SESSIONS.active.uuid)
|
|
306
|
+
return Promise.resolve(MOCK_SESSIONS.active);
|
|
307
|
+
if (sessionId === MOCK_SESSIONS.secondary.uuid)
|
|
308
|
+
return Promise.resolve(MOCK_SESSIONS.secondary);
|
|
309
|
+
if (sessionId === "new-session-uuid")
|
|
310
|
+
return Promise.resolve({
|
|
311
|
+
...MOCK_SESSIONS.active,
|
|
312
|
+
uuid: "new-session-uuid",
|
|
313
|
+
});
|
|
314
|
+
return Promise.resolve(null);
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
mocks.database.spies.getAgentProfileById.mockImplementation(
|
|
319
|
+
(profileId: string) => {
|
|
320
|
+
if (profileId === "agent-profile-1")
|
|
321
|
+
return Promise.resolve(MOCK_AGENT_PROFILES.default);
|
|
322
|
+
if (profileId === "agent-profile-2")
|
|
323
|
+
return Promise.resolve(MOCK_AGENT_PROFILES.custom);
|
|
324
|
+
return Promise.resolve(null);
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
mocks.database.spies.getSavedAgentProfileById.mockImplementation(
|
|
329
|
+
(profileId: string) => {
|
|
330
|
+
if (profileId === "agent-profile-1") {
|
|
331
|
+
return Promise.resolve({
|
|
332
|
+
uuid: "agent-profile-1",
|
|
333
|
+
user_uuid: MOCK_USERS.owner.uuid,
|
|
334
|
+
profile_name: "Default Profile",
|
|
335
|
+
profile: MOCK_AGENT_PROFILES.default,
|
|
336
|
+
preferences: { auto_approve: {} },
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (profileId === "agent-profile-2") {
|
|
340
|
+
return Promise.resolve({
|
|
341
|
+
uuid: "agent-profile-2",
|
|
342
|
+
user_uuid: MOCK_USERS.owner.uuid,
|
|
343
|
+
profile_name: "Custom Profile",
|
|
344
|
+
profile: MOCK_AGENT_PROFILES.custom,
|
|
345
|
+
preferences: { auto_approve: {} },
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return Promise.resolve(null);
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
mocks.database.spies.getUserApiKey.mockImplementation((userId: string) => {
|
|
353
|
+
if (userId === MOCK_USERS.owner.uuid) {
|
|
354
|
+
return Promise.resolve("valid-api-key");
|
|
355
|
+
}
|
|
356
|
+
if (userId === MOCK_USERS.participant.uuid) {
|
|
357
|
+
return Promise.resolve("participant-api-key");
|
|
358
|
+
}
|
|
359
|
+
return Promise.resolve(undefined);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
mocks.database.spies.sessionCreate.mockImplementation(() =>
|
|
363
|
+
Promise.resolve()
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
mocks.database.spies.sessionParticipantAdd.mockImplementation(() =>
|
|
367
|
+
Promise.resolve()
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
mocks.database.spies.sessionParticipantRemove.mockImplementation(() =>
|
|
371
|
+
Promise.resolve()
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// Setup sessionGetParticipants to return participants based on session
|
|
375
|
+
mocks.database.spies.sessionGetParticipants.mockImplementation(
|
|
376
|
+
(sessionId: string) => {
|
|
377
|
+
if (sessionId === MOCK_SESSIONS.active.uuid) {
|
|
378
|
+
return Promise.resolve([
|
|
379
|
+
{ user_uuid: MOCK_USERS.owner.uuid, role: "owner" },
|
|
380
|
+
{ user_uuid: MOCK_USERS.participant.uuid, role: "participant" },
|
|
381
|
+
]);
|
|
382
|
+
}
|
|
383
|
+
if (sessionId === MOCK_SESSIONS.secondary.uuid) {
|
|
384
|
+
return Promise.resolve([
|
|
385
|
+
{ user_uuid: MOCK_USERS.owner.uuid, role: "owner" },
|
|
386
|
+
]);
|
|
387
|
+
}
|
|
388
|
+
return Promise.resolve([]);
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// Setup getUserSessions to return empty array by default
|
|
393
|
+
mocks.database.spies.getUserSessions.mockImplementation(() =>
|
|
394
|
+
Promise.resolve([])
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Setup API key manager mocks
|
|
399
|
+
if (mocks.apiKeyManager) {
|
|
400
|
+
mocks.apiKeyManager.spies.verifyApiKey.mockImplementation(
|
|
401
|
+
(apiKey: string) => {
|
|
402
|
+
if (apiKey === "valid-api-key")
|
|
403
|
+
return Promise.resolve(MOCK_USERS.owner);
|
|
404
|
+
if (apiKey === "participant-api-key")
|
|
405
|
+
return Promise.resolve(MOCK_USERS.participant);
|
|
406
|
+
return Promise.resolve(null);
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Setup user connection manager mocks
|
|
412
|
+
if (mocks.userConnectionManager) {
|
|
413
|
+
mocks.userConnectionManager.spies.getLiveUserApiKey.mockImplementation(
|
|
414
|
+
(userId: string) => {
|
|
415
|
+
if (userId === MOCK_USERS.owner.uuid) return "valid-api-key";
|
|
416
|
+
if (userId === MOCK_USERS.participant.uuid)
|
|
417
|
+
return "participant-api-key";
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection of simple Agent tools.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { OpenAI } from "openai";
|
|
6
|
+
import { Parser } from "expr-eval";
|
|
7
|
+
|
|
8
|
+
import { Agent, IAgentToolProvider, ToolCallResult } from "../../agent/agent";
|
|
9
|
+
import { IPlatform } from "../../agent/iplatform";
|
|
10
|
+
import { htmlToText } from "../utils/htmlToText";
|
|
11
|
+
import { getLogger } from "@xalia/xmcp/sdk";
|
|
12
|
+
import { webSearch } from "../utils/search";
|
|
13
|
+
|
|
14
|
+
const logger = getLogger();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns a function which parses an `args` struct and attempts to extract
|
|
18
|
+
* multiple string parameters with the given names. e.g.
|
|
19
|
+
*
|
|
20
|
+
* const parseFn = makeParseArgsFn(["arg0", "arg1"] as const)
|
|
21
|
+
*
|
|
22
|
+
* creates `parseFn: (args: unknown) => { arg0: string, arg1: string }` which
|
|
23
|
+
* can be used to parse tool arguments.
|
|
24
|
+
*
|
|
25
|
+
* NOTE, the complex type parameters ensures that the name list is a
|
|
26
|
+
* compile-time value, which in turn ensures that the return value of this
|
|
27
|
+
* function is well-typed.
|
|
28
|
+
*/
|
|
29
|
+
export function makeParseArgsFn<
|
|
30
|
+
T extends readonly string[] & (string extends T[number] ? never : unknown),
|
|
31
|
+
>(names: T): (args: unknown) => { [K in T[number]]: string } {
|
|
32
|
+
return (args: unknown) => {
|
|
33
|
+
if (typeof args !== "object") {
|
|
34
|
+
throw new Error(`invalid args: ${typeof args}`);
|
|
35
|
+
}
|
|
36
|
+
const argsObj = args as Record<string, string>;
|
|
37
|
+
for (const name of names) {
|
|
38
|
+
const val = argsObj[name];
|
|
39
|
+
if (typeof val !== "string") {
|
|
40
|
+
throw new Error(`invalid expr args.${name}: ${typeof val}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return argsObj as { [K in T[number]]: string };
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DATETIME_DESC: OpenAI.ChatCompletionTool = {
|
|
48
|
+
type: "function",
|
|
49
|
+
function: {
|
|
50
|
+
name: "time_now",
|
|
51
|
+
description: "Current time",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function isoWithTimezone(timeZone: string): string {
|
|
56
|
+
return (
|
|
57
|
+
new Intl.DateTimeFormat("sv-SE", {
|
|
58
|
+
timeZone,
|
|
59
|
+
year: "numeric",
|
|
60
|
+
month: "2-digit",
|
|
61
|
+
day: "2-digit",
|
|
62
|
+
hour: "2-digit",
|
|
63
|
+
minute: "2-digit",
|
|
64
|
+
second: "2-digit",
|
|
65
|
+
hour12: false,
|
|
66
|
+
timeZoneName: "short",
|
|
67
|
+
})
|
|
68
|
+
.format(new Date())
|
|
69
|
+
.replace(" ", "T") + ` (${timeZone})`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function datetimeTool(timezone: string): IAgentToolProvider {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
75
|
+
const toolFn = async () => {
|
|
76
|
+
return { response: isoWithTimezone(timezone) };
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
80
|
+
setup: async (agent: Agent) => {
|
|
81
|
+
agent.addAgentTool(DATETIME_DESC, toolFn);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ARITHMETIC_DESC: OpenAI.ChatCompletionTool = {
|
|
87
|
+
type: "function",
|
|
88
|
+
function: {
|
|
89
|
+
name: "arithmetic",
|
|
90
|
+
description: "Evaluate arithmetic expression",
|
|
91
|
+
parameters: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
expr: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "Expression containing +-*/()",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: ["expr"],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export function calculatorEval(args: string): string {
|
|
105
|
+
try {
|
|
106
|
+
return String(Parser.evaluate(args));
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (typeof (e as { message: string }).message === "string") {
|
|
109
|
+
return (e as { message: string }).message;
|
|
110
|
+
}
|
|
111
|
+
return String(e);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const calculatorTool: IAgentToolProvider = {
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
117
|
+
setup: async (agent: Agent) => {
|
|
118
|
+
const getExpr = makeParseArgsFn(["expr"] as const);
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
120
|
+
const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
|
|
121
|
+
const { expr } = getExpr(args);
|
|
122
|
+
return { response: calculatorEval(expr) };
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
agent.addAgentTool(ARITHMETIC_DESC, toolFn);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const RENDER_DESC: OpenAI.ChatCompletionTool = {
|
|
130
|
+
type: "function",
|
|
131
|
+
function: {
|
|
132
|
+
name: "render",
|
|
133
|
+
description: "Display the given html fragment inside a div element",
|
|
134
|
+
parameters: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
html: {
|
|
138
|
+
type: "string",
|
|
139
|
+
description: "HTML fragment to render",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
required: ["html"],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export function renderTool(platform: IPlatform): IAgentToolProvider {
|
|
148
|
+
const getHtml = makeParseArgsFn(["html"] as const);
|
|
149
|
+
const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
|
|
150
|
+
const { html } = getHtml(args);
|
|
151
|
+
await platform.renderHTML(html);
|
|
152
|
+
return { response: "" };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
157
|
+
setup: async (agent: Agent) => {
|
|
158
|
+
agent.addAgentTool(RENDER_DESC, toolFn);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const WEB_SEARCH_DESC: OpenAI.ChatCompletionTool = {
|
|
164
|
+
type: "function",
|
|
165
|
+
function: {
|
|
166
|
+
name: "web_search",
|
|
167
|
+
description: "Web search",
|
|
168
|
+
parameters: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
query: {
|
|
172
|
+
type: "string",
|
|
173
|
+
description: "Search query text",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
required: ["query"],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export function webSearchTool(): IAgentToolProvider {
|
|
182
|
+
const getQuery = makeParseArgsFn(["query"] as const);
|
|
183
|
+
const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
|
|
184
|
+
const { query } = getQuery(args);
|
|
185
|
+
logger.debug(`[web_search]: query: ${query}`);
|
|
186
|
+
const results = await webSearch(query);
|
|
187
|
+
logger.debug(`[web_search]: results: ${results}`);
|
|
188
|
+
return { response: JSON.stringify(results) };
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
193
|
+
setup: async (agent: Agent) => {
|
|
194
|
+
agent.addAgentTool(WEB_SEARCH_DESC, toolFn);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// open_url
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* For now, this matches the duckduckgo-mcp-server length. Could extend
|
|
203
|
+
* depending on the application / model etc.
|
|
204
|
+
*/
|
|
205
|
+
const _OPEN_URL_MAX_LENGTH_STR = process.env["OPEN_URL_MAX_LENGTH"] || "8000";
|
|
206
|
+
const OPEN_URL_MAX_LENGTH: number = parseInt(_OPEN_URL_MAX_LENGTH_STR, 10);
|
|
207
|
+
|
|
208
|
+
const OPEN_URL_DESC: OpenAI.ChatCompletionTool = {
|
|
209
|
+
type: "function",
|
|
210
|
+
function: {
|
|
211
|
+
name: "open_url",
|
|
212
|
+
description: "Download content from a URL",
|
|
213
|
+
parameters: {
|
|
214
|
+
type: "object",
|
|
215
|
+
properties: {
|
|
216
|
+
url: {
|
|
217
|
+
type: "string",
|
|
218
|
+
description: "URL to download",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
required: ["url"],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export async function openURL(url: string): Promise<string> {
|
|
227
|
+
const response = await fetch(url);
|
|
228
|
+
if (!response.ok) {
|
|
229
|
+
const status = String(response.status);
|
|
230
|
+
const code = response.statusText;
|
|
231
|
+
throw new Error(`Failed to fetch ${url}: ${status} ${code}`);
|
|
232
|
+
}
|
|
233
|
+
const html = await response.text();
|
|
234
|
+
return htmlToText(html, OPEN_URL_MAX_LENGTH);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function openURLTool(): IAgentToolProvider {
|
|
238
|
+
const getURL = makeParseArgsFn(["url"] as const);
|
|
239
|
+
const toolFn = async (_: Agent, args: unknown): Promise<ToolCallResult> => {
|
|
240
|
+
const { url } = getURL(args);
|
|
241
|
+
return { response: await openURL(url) };
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
246
|
+
setup: async (agent: Agent) => {
|
|
247
|
+
agent.addAgentTool(OPEN_URL_DESC, toolFn);
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Add a set of agent tools for chat sessions.
|
|
254
|
+
*/
|
|
255
|
+
export async function addDefaultChatTools(
|
|
256
|
+
agent: Agent,
|
|
257
|
+
timezone: string,
|
|
258
|
+
platform: IPlatform
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
await agent.addAgentToolProvider(datetimeTool(timezone));
|
|
261
|
+
await agent.addAgentToolProvider(calculatorTool);
|
|
262
|
+
await agent.addAgentToolProvider(renderTool(platform));
|
|
263
|
+
await agent.addAgentToolProvider(webSearchTool());
|
|
264
|
+
await agent.addAgentToolProvider(openURLTool());
|
|
265
|
+
}
|