@xalia/agent 0.5.7 → 0.5.8
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 +3 -0
- package/dist/agent/src/agent/agentUtils.js +6 -12
- package/dist/agent/src/agent/mcpServerManager.js +1 -1
- package/dist/agent/src/agent/openAILLMStreaming.js +14 -7
- package/dist/agent/src/agent/sudoMcpServerManager.js +13 -13
- package/dist/agent/src/chat/client.js +24 -63
- package/dist/agent/src/chat/conversationManager.js +122 -12
- package/dist/agent/src/chat/db.js +9 -0
- package/dist/agent/src/chat/messages.js +27 -0
- package/dist/agent/src/chat/websocket.js +14 -0
- package/dist/agent/src/test/db.test.js +16 -0
- package/dist/agent/src/test/mcpServerManager.test.js +2 -2
- package/dist/agent/src/test/openaiStreaming.test.js +56 -28
- package/dist/agent/src/test/sudoMcpServerManager.test.js +10 -13
- package/dist/agent/src/tool/chatMain.js +7 -1
- package/dist/agent/src/tool/commandPrompt.js +9 -10
- package/package.json +1 -1
- package/src/agent/agent.ts +14 -4
- package/src/agent/agentUtils.ts +17 -23
- package/src/agent/mcpServerManager.ts +3 -1
- package/src/agent/openAILLMStreaming.ts +14 -7
- package/src/agent/sudoMcpServerManager.ts +17 -18
- package/src/chat/client.ts +35 -45
- package/src/chat/conversationManager.ts +147 -12
- package/src/chat/db.ts +14 -0
- package/src/chat/messages.ts +31 -0
- package/src/chat/websocket.ts +14 -0
- package/src/test/db.test.ts +22 -1
- package/src/test/mcpServerManager.test.ts +2 -2
- package/src/test/openaiStreaming.test.ts +64 -30
- package/src/test/sudoMcpServerManager.test.ts +11 -16
- package/src/tool/chatMain.ts +11 -2
- package/src/tool/commandPrompt.ts +8 -15
- package/dist/agent/src/chat/frontendClient.js +0 -74
- package/src/chat/frontendClient.ts +0 -123
package/src/chat/client.ts
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import { strict as assert } from "assert";
|
|
3
|
-
import * as websocket from "ws";
|
|
4
3
|
|
|
5
4
|
import { AgentProfile, McpServerBrief, getLogger } from "@xalia/xmcp/sdk";
|
|
6
5
|
|
|
7
6
|
import { ISkillManager } from "../agent/sudoMcpServerManager";
|
|
8
|
-
import {
|
|
9
|
-
IMcpServerManager,
|
|
10
|
-
McpServerInfo,
|
|
11
|
-
McpServerInfoRW,
|
|
12
|
-
} from "../agent/mcpServerManager";
|
|
13
|
-
|
|
14
|
-
import { ServerToClient, ClientToServer } from "./messages";
|
|
7
|
+
import { McpServerInfo, McpServerInfoRW } from "../agent/mcpServerManager";
|
|
15
8
|
import { ChatCompletionMessageParam, IConversation } from "../agent/agent";
|
|
16
9
|
import { IPlatform } from "../agent/iplatform";
|
|
17
10
|
|
|
11
|
+
import { WebSocket } from "./websocket";
|
|
12
|
+
import { ServerToClient, ClientToServer } from "./messages";
|
|
13
|
+
|
|
18
14
|
const logger = getLogger();
|
|
19
15
|
|
|
20
16
|
type OnMessageCallback = (msg: ServerToClient) => void;
|
|
@@ -25,10 +21,19 @@ interface IMessageSender {
|
|
|
25
21
|
sendMessage(message: ClientToServer): void;
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
class
|
|
24
|
+
class RemoteSudoMcpServerManager implements ISkillManager {
|
|
25
|
+
private sender: IMessageSender;
|
|
26
|
+
private briefs: McpServerBrief[];
|
|
27
|
+
private briefsMap: Record<string, McpServerBrief>;
|
|
29
28
|
private mcpServers: { [serverName: string]: McpServerInfoRW } = {};
|
|
30
29
|
|
|
31
|
-
constructor(
|
|
30
|
+
constructor(sender: IMessageSender, briefs: McpServerBrief[]) {
|
|
31
|
+
this.sender = sender;
|
|
32
|
+
this.briefs = briefs;
|
|
33
|
+
this.briefsMap = {};
|
|
34
|
+
briefs.forEach((b) => {
|
|
35
|
+
this.briefsMap[b.name] = b;
|
|
36
|
+
});
|
|
32
37
|
this.mcpServers = {};
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -161,25 +166,6 @@ class RemoteMcpServerManager implements IMcpServerManager {
|
|
|
161
166
|
|
|
162
167
|
server.disableTool(toolName);
|
|
163
168
|
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
class RemoteSudoMcpServerManager implements ISkillManager {
|
|
167
|
-
private briefsMap: Record<string, McpServerBrief>;
|
|
168
|
-
|
|
169
|
-
constructor(
|
|
170
|
-
private sender: IMessageSender,
|
|
171
|
-
private briefs: McpServerBrief[],
|
|
172
|
-
private msm: RemoteMcpServerManager
|
|
173
|
-
) {
|
|
174
|
-
this.briefsMap = {};
|
|
175
|
-
briefs.forEach((b) => {
|
|
176
|
-
this.briefsMap[b.name] = b;
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
getMcpServerManager(): IMcpServerManager {
|
|
181
|
-
return this.msm;
|
|
182
|
-
}
|
|
183
169
|
|
|
184
170
|
getServerBriefs(): McpServerBrief[] {
|
|
185
171
|
return this.briefs;
|
|
@@ -196,22 +182,23 @@ class RemoteSudoMcpServerManager implements ISkillManager {
|
|
|
196
182
|
enable_all,
|
|
197
183
|
});
|
|
198
184
|
}
|
|
185
|
+
|
|
186
|
+
async shutdown(): Promise<void> {}
|
|
199
187
|
}
|
|
200
188
|
|
|
201
189
|
export class ChatClient implements IMessageSender, IConversation {
|
|
202
190
|
private platform: IPlatform;
|
|
203
|
-
private ws:
|
|
191
|
+
private ws: WebSocket;
|
|
204
192
|
private onMessageCB: OnMessageCallback;
|
|
205
193
|
private onConnectionClosedCB: OnConnectionClosedCallback;
|
|
206
194
|
private closed: boolean;
|
|
207
|
-
private msm: RemoteMcpServerManager;
|
|
208
195
|
private smsm: RemoteSudoMcpServerManager;
|
|
209
196
|
private systemPrompt: string;
|
|
210
197
|
private model: string;
|
|
211
198
|
|
|
212
199
|
private constructor(
|
|
213
200
|
platform: IPlatform,
|
|
214
|
-
ws:
|
|
201
|
+
ws: WebSocket,
|
|
215
202
|
onMessageCB: OnMessageCallback,
|
|
216
203
|
onConnectionClosedCB: OnConnectionClosedCallback,
|
|
217
204
|
serverBriefs: McpServerBrief[]
|
|
@@ -221,8 +208,7 @@ export class ChatClient implements IMessageSender, IConversation {
|
|
|
221
208
|
this.onMessageCB = onMessageCB;
|
|
222
209
|
this.onConnectionClosedCB = onConnectionClosedCB;
|
|
223
210
|
this.closed = false;
|
|
224
|
-
this.
|
|
225
|
-
this.smsm = new RemoteSudoMcpServerManager(this, serverBriefs, this.msm);
|
|
211
|
+
this.smsm = new RemoteSudoMcpServerManager(this, serverBriefs);
|
|
226
212
|
this.systemPrompt = "";
|
|
227
213
|
this.model = "";
|
|
228
214
|
}
|
|
@@ -240,7 +226,7 @@ export class ChatClient implements IMessageSender, IConversation {
|
|
|
240
226
|
const urlParams = new URLSearchParams(params);
|
|
241
227
|
const url = `ws://${host}:${port}?${urlParams}`;
|
|
242
228
|
|
|
243
|
-
const ws = new
|
|
229
|
+
const ws = new WebSocket(url, [token]);
|
|
244
230
|
logger.info("created ws");
|
|
245
231
|
|
|
246
232
|
let client: ChatClient | undefined = undefined;
|
|
@@ -271,7 +257,7 @@ export class ChatClient implements IMessageSender, IConversation {
|
|
|
271
257
|
ws.onopen = async () => {
|
|
272
258
|
logger.info("opened");
|
|
273
259
|
|
|
274
|
-
ws.onmessage = (ev:
|
|
260
|
+
ws.onmessage = (ev: MessageEvent) => {
|
|
275
261
|
try {
|
|
276
262
|
const msgData = ev.data;
|
|
277
263
|
if (typeof msgData !== "string") {
|
|
@@ -288,10 +274,10 @@ export class ChatClient implements IMessageSender, IConversation {
|
|
|
288
274
|
};
|
|
289
275
|
};
|
|
290
276
|
|
|
291
|
-
ws.onclose = async (
|
|
277
|
+
ws.onclose = async (ev: CloseEvent) => {
|
|
292
278
|
logger.info("closed");
|
|
293
279
|
logger.info(
|
|
294
|
-
`[client] WebSocket connection closed: ${JSON.stringify(
|
|
280
|
+
`[client] WebSocket connection closed: ${JSON.stringify(ev)}`
|
|
295
281
|
);
|
|
296
282
|
if (client) {
|
|
297
283
|
client.closed = true;
|
|
@@ -299,9 +285,9 @@ export class ChatClient implements IMessageSender, IConversation {
|
|
|
299
285
|
await onConnectionClosedCB();
|
|
300
286
|
};
|
|
301
287
|
|
|
302
|
-
ws.onerror = async (
|
|
303
|
-
logger.error("[client] WebSocket error:", JSON.stringify(
|
|
304
|
-
e(
|
|
288
|
+
ws.onerror = async (ev: Event) => {
|
|
289
|
+
logger.error("[client] WebSocket error:", JSON.stringify(ev));
|
|
290
|
+
e(ev);
|
|
305
291
|
|
|
306
292
|
if (client) {
|
|
307
293
|
client.closed = true;
|
|
@@ -381,6 +367,10 @@ export class ChatClient implements IMessageSender, IConversation {
|
|
|
381
367
|
throw "resetConversation not implemented for ChatClient";
|
|
382
368
|
}
|
|
383
369
|
|
|
370
|
+
shutdown(): Promise<void> {
|
|
371
|
+
throw "shutdown not implemented for ChatClient";
|
|
372
|
+
}
|
|
373
|
+
|
|
384
374
|
public sendMessage(message: ClientToServer): undefined {
|
|
385
375
|
assert(!this.closed);
|
|
386
376
|
const data = JSON.stringify(message);
|
|
@@ -401,20 +391,20 @@ export class ChatClient implements IMessageSender, IConversation {
|
|
|
401
391
|
//
|
|
402
392
|
|
|
403
393
|
case "mcp_server_added":
|
|
404
|
-
this.
|
|
394
|
+
this.smsm.onMcpServerAdded(
|
|
405
395
|
message.server_name,
|
|
406
396
|
message.tools,
|
|
407
397
|
message.enabled_tools
|
|
408
398
|
);
|
|
409
399
|
break;
|
|
410
400
|
case "mcp_server_removed":
|
|
411
|
-
this.
|
|
401
|
+
this.smsm.onMcpServerRemoved(message.server_name);
|
|
412
402
|
break;
|
|
413
403
|
case "mcp_server_tool_enabled":
|
|
414
|
-
this.
|
|
404
|
+
this.smsm.onMcpServerToolEnabled(message.server_name, message.tool);
|
|
415
405
|
break;
|
|
416
406
|
case "mcp_server_tool_disabled":
|
|
417
|
-
this.
|
|
407
|
+
this.smsm.onMcpServerToolDisabled(message.server_name, message.tool);
|
|
418
408
|
break;
|
|
419
409
|
case "system_prompt_updated":
|
|
420
410
|
this.systemPrompt = message.system_prompt;
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
Agent,
|
|
9
9
|
ChatCompletionMessageParam,
|
|
10
10
|
ChatCompletionMessageToolCall,
|
|
11
|
+
ChatCompletionUserMessageParam,
|
|
12
|
+
createUserMessage,
|
|
11
13
|
} from "../agent/agent";
|
|
12
14
|
import { SkillManager } from "../agent/sudoMcpServerManager";
|
|
13
15
|
import { createAgentWithSkills } from "../agent/agentUtils";
|
|
@@ -21,6 +23,7 @@ import type {
|
|
|
21
23
|
ServerMcpServerToolDisabled,
|
|
22
24
|
ServerSystemPromptUpdated,
|
|
23
25
|
ServerModelUpdated,
|
|
26
|
+
ServerUserMessage,
|
|
24
27
|
} from "./messages";
|
|
25
28
|
import { AsyncQueue } from "./asyncQueue";
|
|
26
29
|
import { Database, UserData } from "./db";
|
|
@@ -81,27 +84,49 @@ export class OpenSession {
|
|
|
81
84
|
throw new UserAlreadyConnected();
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
// Inform any other participants,
|
|
87
|
+
// Inform any other participants, then add the new WebSocket to the map.
|
|
85
88
|
|
|
86
89
|
this.broadcast({ type: "user_joined", user: userName });
|
|
87
90
|
this.connections[userName] = ws;
|
|
88
91
|
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
// - send conversation state (ServerHistory)
|
|
92
|
+
// Send MCP server briefs first (client expects this)
|
|
92
93
|
|
|
93
94
|
const briefs = this.skillManager.getServerBriefs();
|
|
94
95
|
this.sendTo(userName, { type: "mcp_server_briefs", server_briefs: briefs });
|
|
95
96
|
|
|
97
|
+
// Send conversation
|
|
98
|
+
|
|
99
|
+
const conversation = this.agent.getConversation();
|
|
100
|
+
const convMessages = conversationToChatMessages(conversation, userName);
|
|
101
|
+
for (const chatMsg of convMessages) {
|
|
102
|
+
this.sendTo(userName, chatMsg);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Update the MSM state
|
|
106
|
+
|
|
107
|
+
const agentProfile = this.agent.getAgentProfile();
|
|
96
108
|
const msm = this.agent.getMcpServerManager();
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
const
|
|
109
|
+
for (const server_name in agentProfile.mcp_settings) {
|
|
110
|
+
const tools = msm.getMcpServer(server_name).getTools();
|
|
111
|
+
const enabled_tools = agentProfile.mcp_settings[server_name];
|
|
100
112
|
this.sendTo(userName, {
|
|
101
113
|
type: "mcp_server_added",
|
|
102
|
-
server_name
|
|
103
|
-
tools
|
|
104
|
-
enabled_tools
|
|
114
|
+
server_name,
|
|
115
|
+
tools,
|
|
116
|
+
enabled_tools,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Send system prompt and model
|
|
121
|
+
|
|
122
|
+
this.sendTo(userName, {
|
|
123
|
+
type: "system_prompt_updated",
|
|
124
|
+
system_prompt: agentProfile.system_prompt,
|
|
125
|
+
});
|
|
126
|
+
if (agentProfile.model) {
|
|
127
|
+
this.sendTo(userName, {
|
|
128
|
+
type: "model_updated",
|
|
129
|
+
model: agentProfile.model,
|
|
105
130
|
});
|
|
106
131
|
}
|
|
107
132
|
|
|
@@ -215,7 +240,11 @@ export class OpenSession {
|
|
|
215
240
|
});
|
|
216
241
|
}
|
|
217
242
|
|
|
218
|
-
async onChatMessage(
|
|
243
|
+
async onChatMessage(
|
|
244
|
+
message: string,
|
|
245
|
+
// imageB64: string | undefined,
|
|
246
|
+
userToken: string
|
|
247
|
+
): Promise<undefined> {
|
|
219
248
|
// We manually broadcast the user's message here and start the agent
|
|
220
249
|
// conversation, and then wait to get back all data from the agent before
|
|
221
250
|
// processing further messages from clients.
|
|
@@ -225,11 +254,33 @@ export class OpenSession {
|
|
|
225
254
|
type: "user_msg",
|
|
226
255
|
message_id: msgId,
|
|
227
256
|
message,
|
|
257
|
+
// imageB64,
|
|
228
258
|
from: userToken,
|
|
229
259
|
});
|
|
230
260
|
|
|
231
261
|
this.curAgentMsgId = `${msgId}-resp`;
|
|
232
|
-
|
|
262
|
+
const userMessageParam = createUserMessage(
|
|
263
|
+
message,
|
|
264
|
+
/*imageB64*/ undefined,
|
|
265
|
+
userToken
|
|
266
|
+
);
|
|
267
|
+
if (!userMessageParam) {
|
|
268
|
+
logger.debug(`ignoring empty message: ${message}`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const assistantReply = await this.agent.userMessageRaw(userMessageParam);
|
|
273
|
+
if (assistantReply) {
|
|
274
|
+
// TODO: consider including all messages (including tool calls)
|
|
275
|
+
// const newEntries = agent.getTrailingEntries(prevConvLength);
|
|
276
|
+
// await this.db.sessionConversationAppend(this.sessionUUID, newEntries);
|
|
277
|
+
|
|
278
|
+
const newEntries: ChatCompletionMessageParam[] = [
|
|
279
|
+
userMessageParam,
|
|
280
|
+
assistantReply,
|
|
281
|
+
];
|
|
282
|
+
await this.db.sessionConversationAppend(this.sessionUUID, newEntries);
|
|
283
|
+
}
|
|
233
284
|
}
|
|
234
285
|
|
|
235
286
|
async onAddMcpServer(
|
|
@@ -593,3 +644,87 @@ export class ConversationManager {
|
|
|
593
644
|
return openSession;
|
|
594
645
|
}
|
|
595
646
|
}
|
|
647
|
+
|
|
648
|
+
export function userMessageToChatMessage(
|
|
649
|
+
userMessage: ChatCompletionUserMessageParam,
|
|
650
|
+
message_id: string,
|
|
651
|
+
defaultName: string
|
|
652
|
+
): ServerUserMessage | undefined {
|
|
653
|
+
const from = userMessage.name || defaultName;
|
|
654
|
+
if (typeof userMessage.content === "string") {
|
|
655
|
+
return {
|
|
656
|
+
type: "user_msg",
|
|
657
|
+
message_id,
|
|
658
|
+
message: userMessage.content,
|
|
659
|
+
from,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
let message = "";
|
|
664
|
+
let image = undefined;
|
|
665
|
+
for (const content of userMessage.content) {
|
|
666
|
+
switch (content.type) {
|
|
667
|
+
case "text":
|
|
668
|
+
message += content.text;
|
|
669
|
+
break;
|
|
670
|
+
case "image_url":
|
|
671
|
+
assert(!image, "only one image per message supported");
|
|
672
|
+
image = content.image_url;
|
|
673
|
+
break;
|
|
674
|
+
case "input_audio":
|
|
675
|
+
throw "userMessageToChatMessage: audio content not supported";
|
|
676
|
+
case "file":
|
|
677
|
+
throw "userMessageToChatMessage: file content not supported";
|
|
678
|
+
default:
|
|
679
|
+
throw "userMessageToChatMessage: unexpected content.type";
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (image) {
|
|
684
|
+
throw "unimpl: image content";
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
type: "user_msg",
|
|
689
|
+
message_id,
|
|
690
|
+
message,
|
|
691
|
+
from,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export function conversationToChatMessages(
|
|
696
|
+
conversation: ChatCompletionMessageParam[],
|
|
697
|
+
defaultName: string
|
|
698
|
+
): ServerToClient[] {
|
|
699
|
+
const msgs: ServerToClient[] = [];
|
|
700
|
+
for (const ccmp of conversation) {
|
|
701
|
+
const message_id = `message_${msgs.length}`;
|
|
702
|
+
switch (ccmp.role) {
|
|
703
|
+
case "developer":
|
|
704
|
+
throw "developer messages not handled yet";
|
|
705
|
+
case "assistant":
|
|
706
|
+
assert(!ccmp.audio);
|
|
707
|
+
if (ccmp.content) {
|
|
708
|
+
msgs.push({
|
|
709
|
+
type: "agent_msg",
|
|
710
|
+
message: ccmp,
|
|
711
|
+
message_id,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
// TODO: do we want to convert tool calls etc?
|
|
715
|
+
break;
|
|
716
|
+
case "user":
|
|
717
|
+
{
|
|
718
|
+
const msg = userMessageToChatMessage(ccmp, message_id, defaultName);
|
|
719
|
+
if (msg) {
|
|
720
|
+
msgs.push(msg);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
break;
|
|
724
|
+
default:
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return msgs;
|
|
730
|
+
}
|
package/src/chat/db.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getLogger,
|
|
7
7
|
SavedAgentProfile,
|
|
8
8
|
} from "@xalia/xmcp/sdk";
|
|
9
|
+
import { ChatCompletionMessageParam } from "../agent/agent";
|
|
9
10
|
|
|
10
11
|
const logger = getLogger();
|
|
11
12
|
|
|
@@ -284,6 +285,19 @@ export class Database {
|
|
|
284
285
|
return data[0].uuid;
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
async sessionConversationAppend(
|
|
289
|
+
session_id: string,
|
|
290
|
+
newEntries: ChatCompletionMessageParam[]
|
|
291
|
+
): Promise<void> {
|
|
292
|
+
const { error } = await this.client.rpc("session_append_to_conversation", {
|
|
293
|
+
session_id,
|
|
294
|
+
messages: newEntries,
|
|
295
|
+
});
|
|
296
|
+
if (error) {
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
287
301
|
async clearSessions(): Promise<void> {
|
|
288
302
|
await this.client.from("sessions").delete().neq("uuid", "");
|
|
289
303
|
}
|
package/src/chat/messages.ts
CHANGED
|
@@ -233,3 +233,34 @@ export type ServerToClient =
|
|
|
233
233
|
| ServerTyping
|
|
234
234
|
| ServerToClientStateUpdate
|
|
235
235
|
| ServerToClientActions;
|
|
236
|
+
|
|
237
|
+
export function decodeAssistantMessageParam(
|
|
238
|
+
msg: ChatCompletionAssistantMessageParam
|
|
239
|
+
): string {
|
|
240
|
+
let text = "";
|
|
241
|
+
|
|
242
|
+
if (msg.audio) {
|
|
243
|
+
throw "decodeAssistantMessageParam; audio unimplemented";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (msg.content) {
|
|
247
|
+
if (typeof msg.content === "string") {
|
|
248
|
+
text = msg.content;
|
|
249
|
+
} else if (msg.content instanceof Array) {
|
|
250
|
+
for (const c of msg.content) {
|
|
251
|
+
switch (c.type) {
|
|
252
|
+
case "text":
|
|
253
|
+
text += c.text;
|
|
254
|
+
break;
|
|
255
|
+
case "refusal":
|
|
256
|
+
text += c.refusal;
|
|
257
|
+
break;
|
|
258
|
+
default:
|
|
259
|
+
throw Error("unexpected AssistantMessageParam.content entry");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return text;
|
|
266
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Browser and Node.js Universal WebSocket wrapper
|
|
2
|
+
|
|
3
|
+
let WSClass: typeof WebSocket;
|
|
4
|
+
|
|
5
|
+
if (typeof window !== "undefined" && typeof window.WebSocket !== "undefined") {
|
|
6
|
+
// Running in browser
|
|
7
|
+
WSClass = window.WebSocket;
|
|
8
|
+
} else {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
10
|
+
const ws = require("ws");
|
|
11
|
+
WSClass = ws.WebSocket as unknown as typeof WebSocket;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { WSClass as WebSocket };
|
package/src/test/db.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect } from "chai";
|
|
2
2
|
import { Database, SUPABASE_LOCAL_KEY, SUPABASE_LOCAL_URL } from "../chat/db";
|
|
3
|
-
import { AgentProfile } from "../agent/agent";
|
|
3
|
+
import { AgentProfile, ChatCompletionMessageParam } from "../agent/agent";
|
|
4
4
|
import { strict as assert } from "assert";
|
|
5
5
|
import { ApiClient } from "@xalia/xmcp/sdk";
|
|
6
6
|
import { LOCAL_SERVER_URL } from "../agent/sudoMcpServerManager";
|
|
@@ -108,6 +108,15 @@ describe("DB", () => {
|
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
it("should create and retrieve sessions", async function () {
|
|
111
|
+
const CONV_0: ChatCompletionMessageParam[] = [
|
|
112
|
+
{ role: "user", content: "message 0" },
|
|
113
|
+
{ role: "assistant", content: "message 1" },
|
|
114
|
+
];
|
|
115
|
+
const CONV_1: ChatCompletionMessageParam[] = [
|
|
116
|
+
{ role: "user", content: "message 2" },
|
|
117
|
+
{ role: "assistant", content: "message 3" },
|
|
118
|
+
];
|
|
119
|
+
|
|
111
120
|
const db = getLocalDB();
|
|
112
121
|
|
|
113
122
|
await db.clearAgentProfiles();
|
|
@@ -132,5 +141,17 @@ describe("DB", () => {
|
|
|
132
141
|
expect(session.title).eql("test_session");
|
|
133
142
|
expect(session.user_uuid).eql("dummy_user");
|
|
134
143
|
expect(session.uuid).eql(sessionId);
|
|
144
|
+
|
|
145
|
+
// Append messages to empty conversation
|
|
146
|
+
|
|
147
|
+
await db.sessionConversationAppend(sessionId, CONV_0);
|
|
148
|
+
const session0 = (await db.getSessionById(sessionId))!;
|
|
149
|
+
expect(session0.conversation).eql(CONV_0);
|
|
150
|
+
|
|
151
|
+
// Append further messages
|
|
152
|
+
|
|
153
|
+
await db.sessionConversationAppend(sessionId, CONV_1);
|
|
154
|
+
const session1 = (await db.getSessionById(sessionId))!;
|
|
155
|
+
expect(session1.conversation).eql(CONV_0.concat(CONV_1));
|
|
135
156
|
});
|
|
136
157
|
});
|
|
@@ -28,7 +28,7 @@ async function shutdownAll() {
|
|
|
28
28
|
describe("McpServerManager", async () => {
|
|
29
29
|
it("should initialize with correct descriptions", async (): Promise<void> => {
|
|
30
30
|
const tm = getMcpServerManager();
|
|
31
|
-
await tm.
|
|
31
|
+
await tm.addMcpServerWithSSEUrl(
|
|
32
32
|
"simplecalc",
|
|
33
33
|
"http://localhost:5001/mcpservers/sudomcp/simplecalc/session",
|
|
34
34
|
"dummy_key"
|
|
@@ -79,7 +79,7 @@ describe("McpServerManager", async () => {
|
|
|
79
79
|
it("add / remove servers", async () => {
|
|
80
80
|
const tm = getMcpServerManager();
|
|
81
81
|
|
|
82
|
-
await tm.
|
|
82
|
+
await tm.addMcpServerWithSSEUrl(
|
|
83
83
|
"simplecalc",
|
|
84
84
|
"http://localhost:5001/mcpservers/sudomcp/simplecalc/session",
|
|
85
85
|
"dummy_key"
|
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
updateCompletion,
|
|
7
7
|
} from "../agent/openAILLMStreaming";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
|
|
10
|
+
|
|
11
|
+
const TEST_STANDARD: ChatCompletionChunk[] = [
|
|
10
12
|
{
|
|
11
13
|
id: "chatcmpl-BzBqzVs5w0kU5we3KIUN1Q4FAZXjg",
|
|
12
14
|
choices: [
|
|
@@ -34,7 +36,7 @@ const TEST_STANDARD: OpenAI.Chat.Completions.ChatCompletionChunk[] = [
|
|
|
34
36
|
},
|
|
35
37
|
];
|
|
36
38
|
|
|
37
|
-
const TEST_TRAILING_USAGE:
|
|
39
|
+
const TEST_TRAILING_USAGE: ChatCompletionChunk[] = [
|
|
38
40
|
{
|
|
39
41
|
id: "gen-1753923406-nsIKHyFRoJqkUntBnQTw",
|
|
40
42
|
choices: [
|
|
@@ -73,6 +75,55 @@ const TEST_TRAILING_USAGE: OpenAI.Chat.Completions.ChatCompletionChunk[] = [
|
|
|
73
75
|
},
|
|
74
76
|
];
|
|
75
77
|
|
|
78
|
+
const AGGREGATED_MESSAGE: OpenAI.Chat.Completions.ChatCompletion = {
|
|
79
|
+
id: "gen-1753923406-nsIKHyFRoJqkUntBnQTw",
|
|
80
|
+
choices: [
|
|
81
|
+
{
|
|
82
|
+
message: {
|
|
83
|
+
content: "test",
|
|
84
|
+
role: "assistant",
|
|
85
|
+
refusal: null,
|
|
86
|
+
tool_calls: undefined,
|
|
87
|
+
},
|
|
88
|
+
finish_reason: "stop",
|
|
89
|
+
index: 0,
|
|
90
|
+
logprobs: null,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
created: 1753923406,
|
|
94
|
+
model: "openai/gpt-4o",
|
|
95
|
+
object: "chat.completion",
|
|
96
|
+
service_tier: undefined,
|
|
97
|
+
system_fingerprint: "fp_a288987b44",
|
|
98
|
+
usage: {
|
|
99
|
+
completion_tokens: 50,
|
|
100
|
+
prompt_tokens: 271,
|
|
101
|
+
total_tokens: 321,
|
|
102
|
+
completion_tokens_details: { reasoning_tokens: 0 },
|
|
103
|
+
prompt_tokens_details: { cached_tokens: 0 },
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const TEST_TRAILING_USAGE_WITH_3_CHUNKS: ChatCompletionChunk[] = [
|
|
108
|
+
{
|
|
109
|
+
...TEST_TRAILING_USAGE[0],
|
|
110
|
+
choices: [
|
|
111
|
+
{
|
|
112
|
+
delta: { content: "test", role: "assistant" },
|
|
113
|
+
finish_reason: null,
|
|
114
|
+
index: 0,
|
|
115
|
+
logprobs: null,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
...TEST_TRAILING_USAGE[1],
|
|
121
|
+
choices: [{ ...TEST_TRAILING_USAGE[1].choices[0], finish_reason: "stop" }],
|
|
122
|
+
usage: undefined,
|
|
123
|
+
},
|
|
124
|
+
{ ...TEST_TRAILING_USAGE[1], choices: [] },
|
|
125
|
+
];
|
|
126
|
+
|
|
76
127
|
describe("OpenAI Streaming", () => {
|
|
77
128
|
it("should support standard termination", async function () {
|
|
78
129
|
const chunks = TEST_STANDARD;
|
|
@@ -110,33 +161,16 @@ describe("OpenAI Streaming", () => {
|
|
|
110
161
|
const { initMessage } = initializeCompletion(chunks[0]);
|
|
111
162
|
updateCompletion(initMessage, chunks[1]);
|
|
112
163
|
|
|
113
|
-
expect(initMessage).eql(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
index: 0,
|
|
125
|
-
logprobs: null,
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
created: 1753923406,
|
|
129
|
-
model: "openai/gpt-4o",
|
|
130
|
-
object: "chat.completion",
|
|
131
|
-
service_tier: undefined,
|
|
132
|
-
system_fingerprint: "fp_a288987b44",
|
|
133
|
-
usage: {
|
|
134
|
-
completion_tokens: 50,
|
|
135
|
-
prompt_tokens: 271,
|
|
136
|
-
total_tokens: 321,
|
|
137
|
-
completion_tokens_details: { reasoning_tokens: 0 },
|
|
138
|
-
prompt_tokens_details: { cached_tokens: 0 },
|
|
139
|
-
},
|
|
140
|
-
});
|
|
164
|
+
expect(initMessage).eql(AGGREGATED_MESSAGE);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should support trailing usage with 3 chunks", async function () {
|
|
168
|
+
const chunks = TEST_TRAILING_USAGE_WITH_3_CHUNKS;
|
|
169
|
+
expect(chunks.length).eql(3);
|
|
170
|
+
const { initMessage } = initializeCompletion(chunks[0]);
|
|
171
|
+
updateCompletion(initMessage, chunks[1]);
|
|
172
|
+
updateCompletion(initMessage, chunks[2]);
|
|
173
|
+
|
|
174
|
+
expect(initMessage).eql(AGGREGATED_MESSAGE);
|
|
141
175
|
});
|
|
142
176
|
});
|