@xalia/agent 0.5.4 → 0.5.5
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 +16 -9
- package/dist/agent/src/agent/agentUtils.js +24 -4
- package/dist/agent/src/agent/mcpServerManager.js +19 -9
- package/dist/agent/src/agent/openAILLM.js +3 -1
- package/dist/agent/src/agent/openAILLMStreaming.js +24 -25
- package/dist/agent/src/agent/repeatLLM.js +43 -0
- package/dist/agent/src/agent/sudoMcpServerManager.js +12 -6
- package/dist/agent/src/chat/client.js +259 -36
- package/dist/agent/src/chat/conversationManager.js +243 -24
- package/dist/agent/src/chat/db.js +24 -1
- package/dist/agent/src/chat/frontendClient.js +74 -0
- package/dist/agent/src/chat/server.js +3 -3
- package/dist/agent/src/test/db.test.js +25 -2
- package/dist/agent/src/test/openaiStreaming.test.js +133 -0
- package/dist/agent/src/test/prompt.test.js +2 -2
- package/dist/agent/src/test/sudoMcpServerManager.test.js +1 -1
- package/dist/agent/src/tool/agentChat.js +7 -197
- package/dist/agent/src/tool/chatMain.js +18 -23
- package/dist/agent/src/tool/commandPrompt.js +248 -0
- package/dist/agent/src/tool/prompt.js +27 -31
- package/package.json +1 -1
- package/scripts/test_chat +17 -1
- package/src/agent/agent.ts +34 -11
- package/src/agent/agentUtils.ts +52 -3
- package/src/agent/mcpServerManager.ts +43 -13
- package/src/agent/openAILLM.ts +3 -1
- package/src/agent/openAILLMStreaming.ts +28 -27
- package/src/agent/repeatLLM.ts +51 -0
- package/src/agent/sudoMcpServerManager.ts +41 -12
- package/src/chat/client.ts +353 -40
- package/src/chat/conversationManager.ts +345 -33
- package/src/chat/db.ts +28 -2
- package/src/chat/frontendClient.ts +123 -0
- package/src/chat/messages.ts +146 -2
- package/src/chat/server.ts +3 -3
- package/src/test/db.test.ts +35 -2
- package/src/test/openaiStreaming.test.ts +142 -0
- package/src/test/prompt.test.ts +1 -1
- package/src/test/sudoMcpServerManager.test.ts +1 -1
- package/src/tool/agentChat.ts +13 -211
- package/src/tool/chatMain.ts +28 -43
- package/src/tool/commandPrompt.ts +252 -0
- package/src/tool/prompt.ts +33 -32
@@ -15,11 +15,17 @@ import { createAgentWithSkills } from "../agent/agentUtils";
|
|
15
15
|
import type {
|
16
16
|
ClientToServer,
|
17
17
|
ServerToClient,
|
18
|
-
|
18
|
+
ServerMcpServerAdded,
|
19
|
+
ServerMcpServerRemoved,
|
20
|
+
ServerMcpServerToolEnabled,
|
21
|
+
ServerMcpServerToolDisabled,
|
22
|
+
ServerSystemPromptUpdated,
|
23
|
+
ServerModelUpdated,
|
19
24
|
} from "./messages";
|
20
25
|
import { AsyncQueue } from "./asyncQueue";
|
21
26
|
import { Database, UserData } from "./db";
|
22
27
|
import { AsyncLock } from "../utils/asyncLock";
|
28
|
+
import { McpServerInfo, McpServerManager } from "../agent/mcpServerManager";
|
23
29
|
|
24
30
|
const logger = getLogger();
|
25
31
|
|
@@ -38,20 +44,29 @@ type QueuedClientMessage<T extends ClientToServer = ClientToServer> = {
|
|
38
44
|
* Describes a Session (conversation) with connected participants.
|
39
45
|
*/
|
40
46
|
export class OpenSession {
|
47
|
+
public db: Database;
|
41
48
|
public onEmpty: () => void;
|
49
|
+
/// Map of user identifier to connection
|
42
50
|
public connections: Record<string, ws.WebSocket>;
|
43
51
|
public agent: Agent;
|
52
|
+
public sessionUUID: string;
|
53
|
+
public agentProfileUUID: string;
|
44
54
|
public skillManager: SkillManager;
|
45
55
|
public messageQueue: AsyncQueue<QueuedClientMessage>;
|
46
56
|
public curAgentMsgId: string | undefined;
|
47
57
|
|
48
58
|
constructor(
|
59
|
+
db: Database,
|
49
60
|
agent: Agent,
|
61
|
+
sessionUUID: string,
|
62
|
+
agentProfileUUID: string,
|
50
63
|
sudoMcpServerManager: SkillManager,
|
51
64
|
onEmpty: () => void
|
52
65
|
) {
|
53
|
-
|
66
|
+
this.db = db;
|
54
67
|
this.agent = agent;
|
68
|
+
this.sessionUUID = sessionUUID;
|
69
|
+
this.agentProfileUUID = agentProfileUUID;
|
55
70
|
this.skillManager = sudoMcpServerManager;
|
56
71
|
this.onEmpty = onEmpty;
|
57
72
|
this.connections = {};
|
@@ -71,6 +86,25 @@ export class OpenSession {
|
|
71
86
|
this.broadcast({ type: "user_joined", user: userName });
|
72
87
|
this.connections[userName] = ws;
|
73
88
|
|
89
|
+
// TODO:
|
90
|
+
//
|
91
|
+
// - send conversation state (ServerHistory)
|
92
|
+
|
93
|
+
const briefs = this.skillManager.getServerBriefs();
|
94
|
+
this.sendTo(userName, { type: "mcp_server_briefs", server_briefs: briefs });
|
95
|
+
|
96
|
+
const msm = this.agent.getMcpServerManager();
|
97
|
+
const mcpServerNames = msm.getMcpServerNames();
|
98
|
+
for (const serverName of mcpServerNames) {
|
99
|
+
const info = msm.getMcpServer(serverName);
|
100
|
+
this.sendTo(userName, {
|
101
|
+
type: "mcp_server_added",
|
102
|
+
server_name: serverName,
|
103
|
+
tools: info.getTools(),
|
104
|
+
enabled_tools: Object.keys(info.getEnabledTools()),
|
105
|
+
});
|
106
|
+
}
|
107
|
+
|
74
108
|
ws.on("message", async (message: ws.RawData) => {
|
75
109
|
logger.debug(`[convMgr]: got message: (from ${userName}): ${message}`);
|
76
110
|
const msgStr = message.toString();
|
@@ -102,14 +136,71 @@ export class OpenSession {
|
|
102
136
|
`${JSON.stringify(queuedMessage.msg)}`
|
103
137
|
);
|
104
138
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
139
|
+
// In general, handlers return a message to be broadcast. Errors are
|
140
|
+
// handled by returning an error to just the sender. Handlers can
|
141
|
+
// also broadcast and send directly.
|
142
|
+
|
143
|
+
try {
|
144
|
+
const msg = queuedMessage.msg;
|
145
|
+
let broadcastMsg: ServerToClient[] | ServerToClient | undefined =
|
146
|
+
undefined;
|
147
|
+
switch (msg.type) {
|
148
|
+
case "msg":
|
149
|
+
broadcastMsg = await this.onChatMessage(
|
150
|
+
msg.message,
|
151
|
+
queuedMessage.from
|
152
|
+
);
|
153
|
+
break;
|
154
|
+
case "add_mcp_server":
|
155
|
+
broadcastMsg = await this.onAddMcpServer(
|
156
|
+
msg.server_name,
|
157
|
+
msg.enable_all
|
158
|
+
);
|
159
|
+
break;
|
160
|
+
case "remove_mcp_server":
|
161
|
+
broadcastMsg = await this.onRemoveMcpServer(msg.server_name);
|
162
|
+
break;
|
163
|
+
case "enable_mcp_server_tool":
|
164
|
+
broadcastMsg = await this.onEnableMcpServerTool(
|
165
|
+
msg.server_name,
|
166
|
+
msg.tool
|
167
|
+
);
|
168
|
+
break;
|
169
|
+
case "disable_mcp_server_tool":
|
170
|
+
broadcastMsg = await this.onDisableMcpServerTool(
|
171
|
+
msg.server_name,
|
172
|
+
msg.tool
|
173
|
+
);
|
174
|
+
break;
|
175
|
+
case "enable_all_mcp_server_tools":
|
176
|
+
broadcastMsg = await this.onEnableAllMcpServerTools(msg.server_name);
|
177
|
+
break;
|
178
|
+
case "disable_all_mcp_server_tools":
|
179
|
+
broadcastMsg = await this.onDisableAllMcpServerTools(msg.server_name);
|
180
|
+
break;
|
181
|
+
case "set_system_prompt":
|
182
|
+
broadcastMsg = await this.onSetSystemPrompt(msg.system_prompt);
|
183
|
+
break;
|
184
|
+
case "set_model":
|
185
|
+
broadcastMsg = await this.onSetModel(msg.model);
|
186
|
+
break;
|
187
|
+
default:
|
188
|
+
throw `unknown message: ${JSON.stringify(queuedMessage)}`;
|
189
|
+
}
|
190
|
+
|
191
|
+
if (broadcastMsg) {
|
192
|
+
if (broadcastMsg instanceof Array) {
|
193
|
+
broadcastMsg.map((msg) => this.broadcast(msg));
|
194
|
+
} else {
|
195
|
+
this.broadcast(broadcastMsg);
|
196
|
+
}
|
197
|
+
}
|
198
|
+
} catch (err: unknown) {
|
199
|
+
if (typeof err === "string") {
|
200
|
+
this.sendTo(queuedMessage.from, { type: "error", message: err });
|
201
|
+
} else {
|
202
|
+
throw err;
|
203
|
+
}
|
113
204
|
}
|
114
205
|
}
|
115
206
|
|
@@ -124,24 +215,163 @@ export class OpenSession {
|
|
124
215
|
});
|
125
216
|
}
|
126
217
|
|
127
|
-
async onChatMessage(
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
218
|
+
async onChatMessage(message: string, userToken: string): Promise<undefined> {
|
219
|
+
// We manually broadcast the user's message here and start the agent
|
220
|
+
// conversation, and then wait to get back all data from the agent before
|
221
|
+
// processing further messages from clients.
|
222
|
+
|
132
223
|
const msgId = uuidv4();
|
133
224
|
this.broadcast({
|
134
225
|
type: "user_msg",
|
135
226
|
message_id: msgId,
|
136
|
-
message
|
227
|
+
message,
|
137
228
|
from: userToken,
|
138
229
|
});
|
139
230
|
|
140
|
-
// Messages will be handled by the Agent.onMessage callback. We await the
|
141
|
-
// response here before processing further messages.
|
142
|
-
|
143
231
|
this.curAgentMsgId = `${msgId}-resp`;
|
144
|
-
await this.agent.
|
232
|
+
await this.agent.userMessageEx(message, undefined, userToken);
|
233
|
+
}
|
234
|
+
|
235
|
+
async onAddMcpServer(
|
236
|
+
serverName: string,
|
237
|
+
enableAll: boolean
|
238
|
+
): Promise<ServerMcpServerAdded> {
|
239
|
+
logger.info(
|
240
|
+
`[onAddMcpServer]: Adding server ${serverName} (enable_all: ${enableAll})`
|
241
|
+
);
|
242
|
+
|
243
|
+
const mcpServerManager = this.agent.getMcpServerManager();
|
244
|
+
if (mcpServerManager.hasMcpServer(serverName)) {
|
245
|
+
throw `${serverName} already added`;
|
246
|
+
}
|
247
|
+
|
248
|
+
if (!this.skillManager.hasServer(serverName)) {
|
249
|
+
throw `no such server: ${serverName}`;
|
250
|
+
}
|
251
|
+
|
252
|
+
await this.skillManager.addMcpServer(serverName, enableAll);
|
253
|
+
mcpServerManager.enableAllTools(serverName);
|
254
|
+
|
255
|
+
// Save changes to the AgentProfile
|
256
|
+
|
257
|
+
await this.updateAgentProfile();
|
258
|
+
|
259
|
+
// Broadcast the message to all participants.
|
260
|
+
|
261
|
+
const server = mcpServerManager.getMcpServer(serverName);
|
262
|
+
const tools = server.getTools();
|
263
|
+
const enabled_tools = Object.keys(server.getEnabledTools());
|
264
|
+
return {
|
265
|
+
type: "mcp_server_added",
|
266
|
+
server_name: serverName,
|
267
|
+
tools,
|
268
|
+
enabled_tools,
|
269
|
+
};
|
270
|
+
}
|
271
|
+
|
272
|
+
async onRemoveMcpServer(
|
273
|
+
server_name: string
|
274
|
+
): Promise<ServerMcpServerRemoved> {
|
275
|
+
logger.info(`[onRemoveMcpServer]: Removing server ${server_name}`);
|
276
|
+
|
277
|
+
const mcpServerManager = this.agent.getMcpServerManager();
|
278
|
+
if (!mcpServerManager.hasMcpServer(server_name)) {
|
279
|
+
throw `${server_name} not enabled`;
|
280
|
+
}
|
281
|
+
|
282
|
+
await mcpServerManager.removeMcpServer(server_name);
|
283
|
+
await this.updateAgentProfile();
|
284
|
+
|
285
|
+
return {
|
286
|
+
type: "mcp_server_removed",
|
287
|
+
server_name,
|
288
|
+
};
|
289
|
+
}
|
290
|
+
|
291
|
+
async onEnableMcpServerTool(
|
292
|
+
server_name: string,
|
293
|
+
tool: string
|
294
|
+
): Promise<ServerMcpServerToolEnabled> {
|
295
|
+
const msm = this.agent.getMcpServerManager();
|
296
|
+
this.ensureMcpServerAndTool(msm, server_name, tool);
|
297
|
+
msm.enableTool(server_name, tool);
|
298
|
+
|
299
|
+
await this.updateAgentProfile();
|
300
|
+
|
301
|
+
return { type: "mcp_server_tool_enabled", server_name, tool };
|
302
|
+
}
|
303
|
+
|
304
|
+
async onDisableMcpServerTool(
|
305
|
+
server_name: string,
|
306
|
+
tool: string
|
307
|
+
): Promise<ServerMcpServerToolDisabled> {
|
308
|
+
const msm = this.agent.getMcpServerManager();
|
309
|
+
this.ensureMcpServerAndTool(msm, server_name, tool);
|
310
|
+
msm.disableTool(server_name, tool);
|
311
|
+
|
312
|
+
await this.updateAgentProfile();
|
313
|
+
|
314
|
+
return { type: "mcp_server_tool_disabled", server_name, tool };
|
315
|
+
}
|
316
|
+
|
317
|
+
async onEnableAllMcpServerTools(
|
318
|
+
server_name: string
|
319
|
+
): Promise<ServerMcpServerToolEnabled[]> {
|
320
|
+
// We reimplement the logic to enable any disabled tools so we can
|
321
|
+
// construct messages along the way.
|
322
|
+
|
323
|
+
const msm = this.agent.getMcpServerManager();
|
324
|
+
const server = this.ensureMcpServer(msm, server_name);
|
325
|
+
const enabledTools = server.getEnabledTools();
|
326
|
+
const msgs: ServerMcpServerToolEnabled[] = [];
|
327
|
+
for (const tool of server.getTools()) {
|
328
|
+
if (!enabledTools[tool.name]) {
|
329
|
+
msm.enableTool(server_name, tool.name);
|
330
|
+
msgs.push({
|
331
|
+
type: "mcp_server_tool_enabled",
|
332
|
+
server_name,
|
333
|
+
tool: tool.name,
|
334
|
+
});
|
335
|
+
}
|
336
|
+
}
|
337
|
+
|
338
|
+
await this.updateAgentProfile();
|
339
|
+
|
340
|
+
return msgs;
|
341
|
+
}
|
342
|
+
|
343
|
+
async onDisableAllMcpServerTools(
|
344
|
+
server_name: string
|
345
|
+
): Promise<ServerMcpServerToolDisabled[]> {
|
346
|
+
// We reimplement the logic to disable all enabled tools so we can
|
347
|
+
// construct messages along the way.
|
348
|
+
|
349
|
+
const msm = this.agent.getMcpServerManager();
|
350
|
+
const server = this.ensureMcpServer(msm, server_name);
|
351
|
+
const enabledTools = server.getEnabledTools();
|
352
|
+
const msgs: ServerMcpServerToolDisabled[] = [];
|
353
|
+
for (const tool in enabledTools) {
|
354
|
+
msm.disableTool(server_name, tool);
|
355
|
+
msgs.push({ type: "mcp_server_tool_disabled", server_name, tool });
|
356
|
+
}
|
357
|
+
|
358
|
+
await this.updateAgentProfile();
|
359
|
+
|
360
|
+
return msgs;
|
361
|
+
}
|
362
|
+
|
363
|
+
async onSetSystemPrompt(
|
364
|
+
system_prompt: string
|
365
|
+
): Promise<ServerSystemPromptUpdated> {
|
366
|
+
this.agent.setSystemPrompt(system_prompt);
|
367
|
+
await this.updateAgentProfile();
|
368
|
+
return { type: "system_prompt_updated", system_prompt };
|
369
|
+
}
|
370
|
+
|
371
|
+
async onSetModel(model: string): Promise<ServerModelUpdated> {
|
372
|
+
this.agent.setModel(model);
|
373
|
+
await this.updateAgentProfile();
|
374
|
+
return { type: "model_updated", model };
|
145
375
|
}
|
146
376
|
|
147
377
|
broadcast(msg: ServerToClient) {
|
@@ -151,6 +381,47 @@ export class OpenSession {
|
|
151
381
|
ws.send(msgString);
|
152
382
|
}
|
153
383
|
}
|
384
|
+
|
385
|
+
sendTo(userName: string, msg: ServerToClient) {
|
386
|
+
const ws = this.connections[userName];
|
387
|
+
const msgString = JSON.stringify(msg);
|
388
|
+
logger.info(`[sendTo]: (${userName}) msg: ${msgString}`);
|
389
|
+
assert(ws);
|
390
|
+
ws.send(msgString);
|
391
|
+
}
|
392
|
+
|
393
|
+
private ensureMcpServer(
|
394
|
+
msm: McpServerManager,
|
395
|
+
serverName: string
|
396
|
+
): McpServerInfo {
|
397
|
+
const server = msm.getMcpServer(serverName);
|
398
|
+
if (!server) {
|
399
|
+
throw `${serverName} not added`;
|
400
|
+
}
|
401
|
+
return server;
|
402
|
+
}
|
403
|
+
|
404
|
+
private ensureMcpServerAndTool(
|
405
|
+
msm: McpServerManager,
|
406
|
+
serverName: string,
|
407
|
+
toolName: string
|
408
|
+
) {
|
409
|
+
const server = this.ensureMcpServer(msm, serverName);
|
410
|
+
const tool = server.getTool(toolName);
|
411
|
+
if (!tool) {
|
412
|
+
throw `Tool ${toolName} on ${serverName} not found`;
|
413
|
+
}
|
414
|
+
return tool;
|
415
|
+
}
|
416
|
+
|
417
|
+
private async updateAgentProfile(): Promise<void> {
|
418
|
+
const profile = this.agent.getAgentProfile();
|
419
|
+
logger.debug(
|
420
|
+
`[updateAgentProfile]: uuid: ${this.agentProfileUUID} profile: ` +
|
421
|
+
JSON.stringify(profile)
|
422
|
+
);
|
423
|
+
return this.db.updateAgentProfile(this.agentProfileUUID, profile);
|
424
|
+
}
|
154
425
|
}
|
155
426
|
|
156
427
|
/**
|
@@ -184,11 +455,12 @@ export class ConversationManager {
|
|
184
455
|
sessionId,
|
185
456
|
llmApiKey,
|
186
457
|
xmcpApiKey,
|
187
|
-
userData.nickname || userData.
|
458
|
+
userData.nickname || userData.uuid,
|
188
459
|
ws
|
189
460
|
);
|
190
461
|
});
|
191
462
|
}
|
463
|
+
|
192
464
|
/**
|
193
465
|
* Must be called while holding the openSessionsLock
|
194
466
|
*/
|
@@ -223,29 +495,56 @@ export class ConversationManager {
|
|
223
495
|
const conversation =
|
224
496
|
sessionData.conversation as unknown as ChatCompletionMessageParam[];
|
225
497
|
|
226
|
-
const
|
227
|
-
|
228
|
-
);
|
498
|
+
const agentProfileUUID = sessionData.agent_profile_uuid;
|
499
|
+
const agentProfile = await this.db.getAgentProfileById(agentProfileUUID);
|
229
500
|
if (!agentProfile) {
|
230
|
-
throw `no such agent profile ${
|
501
|
+
throw `no such agent profile ${agentProfileUUID}`;
|
231
502
|
}
|
232
503
|
|
504
|
+
// TODO: store some owner data on the OpenSession object itself?
|
505
|
+
|
506
|
+
const owner = await this.db.getUserFromUuid(sessionData.user_uuid);
|
507
|
+
assert(owner, `no owner for session ${JSON.stringify(sessionData)}`);
|
508
|
+
const ownerUserName = owner.nickname;
|
509
|
+
if (!ownerUserName) {
|
510
|
+
throw (
|
511
|
+
`user ${sessionData.user_uuid} has no user name - ` +
|
512
|
+
"cannot create chat session"
|
513
|
+
);
|
514
|
+
}
|
515
|
+
|
516
|
+
// Access to the OpenSession (once it is iniailized
|
517
|
+
const context: { openSession?: OpenSession } = {};
|
518
|
+
|
233
519
|
const platform = {
|
234
520
|
openUrl: (
|
235
|
-
|
521
|
+
url: string,
|
236
522
|
_authResultP: Promise<boolean>,
|
237
|
-
|
523
|
+
display_name: string
|
238
524
|
) => {
|
239
|
-
|
525
|
+
// These requests are always passed to the original owner, since it is
|
526
|
+
// his settings that will be used for all mcp servers.
|
527
|
+
|
528
|
+
if (context.openSession) {
|
529
|
+
const conn = openSession.connections[ownerUserName];
|
530
|
+
if (conn) {
|
531
|
+
openSession.sendTo(ownerUserName, {
|
532
|
+
type: "open_url",
|
533
|
+
url,
|
534
|
+
display_name,
|
535
|
+
});
|
536
|
+
} else {
|
537
|
+
throw `user ${ownerUserName} must authenticate`;
|
538
|
+
}
|
539
|
+
} else {
|
540
|
+
throw `no open session ${sessionData.uuid}`;
|
541
|
+
}
|
240
542
|
},
|
241
543
|
load: (_filename: string): Promise<string> => {
|
242
|
-
throw "unimpl";
|
544
|
+
throw "unimpl platform.load";
|
243
545
|
},
|
244
546
|
};
|
245
547
|
|
246
|
-
// Forward agent messages to the OpenSession (once it is iniailized
|
247
|
-
|
248
|
-
const context: { openSession?: OpenSession } = {};
|
249
548
|
const onMessage = async (msg: string, end: boolean) => {
|
250
549
|
logger.debug(`[onMessage] msg: ${msg}, end: ${end}`);
|
251
550
|
assert(context.openSession);
|
@@ -256,6 +555,12 @@ export class ConversationManager {
|
|
256
555
|
toolCall: ChatCompletionMessageToolCall
|
257
556
|
): Promise<boolean> => {
|
258
557
|
logger.debug(`[onToolCall] : ${JSON.stringify(toolCall)}`);
|
558
|
+
assert(context.openSession);
|
559
|
+
context.openSession.broadcast({
|
560
|
+
type: "agent_tool_call",
|
561
|
+
message_id: toolCall.id,
|
562
|
+
message: toolCall,
|
563
|
+
});
|
259
564
|
return true;
|
260
565
|
};
|
261
566
|
|
@@ -274,7 +579,14 @@ export class ConversationManager {
|
|
274
579
|
true
|
275
580
|
);
|
276
581
|
|
277
|
-
openSession = new OpenSession(
|
582
|
+
openSession = new OpenSession(
|
583
|
+
this.db,
|
584
|
+
agent,
|
585
|
+
sessionId,
|
586
|
+
agentProfileUUID,
|
587
|
+
smsm,
|
588
|
+
onEmpty
|
589
|
+
);
|
278
590
|
context.openSession = openSession;
|
279
591
|
this.openSessions[sessionId] = openSession;
|
280
592
|
openSession.join(userName, ws);
|
package/src/chat/db.ts
CHANGED
@@ -31,7 +31,7 @@ export function resolveCompoundName(name: string): string | [string, string] {
|
|
31
31
|
}
|
32
32
|
|
33
33
|
export type UserData = {
|
34
|
-
|
34
|
+
uuid: supabase.Tables<"api_keys">["user_uuid"];
|
35
35
|
nickname: supabase.Tables<"users">["nickname"];
|
36
36
|
};
|
37
37
|
|
@@ -67,11 +67,24 @@ export class Database {
|
|
67
67
|
}
|
68
68
|
|
69
69
|
return {
|
70
|
-
|
70
|
+
uuid: data.user_uuid,
|
71
71
|
nickname: data.users.nickname || `user ${data.user_uuid}`,
|
72
72
|
};
|
73
73
|
}
|
74
74
|
|
75
|
+
async getUserFromUuid(user_uuid: string): Promise<UserData | undefined> {
|
76
|
+
const { data, error } = await this.client
|
77
|
+
.from("users")
|
78
|
+
.select("*")
|
79
|
+
.eq("uuid", user_uuid)
|
80
|
+
.maybeSingle();
|
81
|
+
if (error) {
|
82
|
+
throw error;
|
83
|
+
}
|
84
|
+
|
85
|
+
return data;
|
86
|
+
}
|
87
|
+
|
75
88
|
async createUser(
|
76
89
|
user_uuid: string,
|
77
90
|
email: string,
|
@@ -196,6 +209,19 @@ export class Database {
|
|
196
209
|
return data[0].uuid;
|
197
210
|
}
|
198
211
|
|
212
|
+
async updateAgentProfile(uuid: string, profile: AgentProfile): Promise<void> {
|
213
|
+
const payload: supabase.TablesUpdate<"agent_profiles"> = {
|
214
|
+
profile: profile as unknown as supabase.Json,
|
215
|
+
};
|
216
|
+
const { error } = await this.client
|
217
|
+
.from("agent_profiles")
|
218
|
+
.update(payload)
|
219
|
+
.eq("uuid", uuid);
|
220
|
+
if (error) {
|
221
|
+
throw error;
|
222
|
+
}
|
223
|
+
}
|
224
|
+
|
199
225
|
async clearAgentProfiles(): Promise<void> {
|
200
226
|
await this.client.from("agent_profiles").delete().neq("uuid", "");
|
201
227
|
}
|
@@ -0,0 +1,123 @@
|
|
1
|
+
import { ServerToClient, ClientToServer } from "./messages";
|
2
|
+
|
3
|
+
type OnMessageCallback = (msg: ServerToClient) => void;
|
4
|
+
type OnConnectionClosedCallback = () => void;
|
5
|
+
|
6
|
+
export class FrontendChatClient {
|
7
|
+
private ws: WebSocket;
|
8
|
+
private onMessageCB: OnMessageCallback;
|
9
|
+
private onConnectionClosedCB: OnConnectionClosedCallback;
|
10
|
+
private closed: boolean;
|
11
|
+
|
12
|
+
private constructor(
|
13
|
+
ws: WebSocket,
|
14
|
+
onMessageCB: OnMessageCallback,
|
15
|
+
onConnectionClosedCB: OnConnectionClosedCallback
|
16
|
+
) {
|
17
|
+
this.ws = ws;
|
18
|
+
this.onMessageCB = onMessageCB;
|
19
|
+
this.onConnectionClosedCB = onConnectionClosedCB;
|
20
|
+
this.closed = false;
|
21
|
+
}
|
22
|
+
|
23
|
+
static async initWithParams(
|
24
|
+
host: string,
|
25
|
+
port: number,
|
26
|
+
token: string,
|
27
|
+
params: Record<string, string>,
|
28
|
+
onMessageCB: OnMessageCallback,
|
29
|
+
onConnectionClosedCB: OnConnectionClosedCallback
|
30
|
+
): Promise<FrontendChatClient> {
|
31
|
+
return new Promise((resolve, reject) => {
|
32
|
+
const urlParams = new URLSearchParams(params);
|
33
|
+
const url = `ws://${host}:${port}?${urlParams}`;
|
34
|
+
|
35
|
+
const ws = new WebSocket(url, [token]);
|
36
|
+
console.log("created ws");
|
37
|
+
|
38
|
+
const client = new FrontendChatClient(
|
39
|
+
ws,
|
40
|
+
onMessageCB,
|
41
|
+
onConnectionClosedCB
|
42
|
+
);
|
43
|
+
|
44
|
+
ws.onopen = async () => {
|
45
|
+
console.log("opened");
|
46
|
+
|
47
|
+
ws.onmessage = (ev: MessageEvent) => {
|
48
|
+
try {
|
49
|
+
const msgData = ev.data;
|
50
|
+
if (typeof msgData !== "string") {
|
51
|
+
throw `expected "string" data, got ${typeof msgData}`;
|
52
|
+
}
|
53
|
+
console.debug(`[client.onmessage]: ${msgData}`);
|
54
|
+
const msg: ServerToClient = JSON.parse(msgData);
|
55
|
+
client.onMessageCB(msg);
|
56
|
+
} catch (e) {
|
57
|
+
client.close();
|
58
|
+
throw e;
|
59
|
+
}
|
60
|
+
};
|
61
|
+
resolve(client);
|
62
|
+
};
|
63
|
+
|
64
|
+
ws.onclose = (event) => {
|
65
|
+
console.log("closed");
|
66
|
+
console.log(
|
67
|
+
`[client] WebSocket connection closed: ${JSON.stringify(event)}`
|
68
|
+
);
|
69
|
+
client.closed = true;
|
70
|
+
onConnectionClosedCB();
|
71
|
+
};
|
72
|
+
|
73
|
+
ws.onerror = (error) => {
|
74
|
+
console.error("[client] WebSocket error:", error);
|
75
|
+
reject(error);
|
76
|
+
|
77
|
+
client.closed = true;
|
78
|
+
onConnectionClosedCB();
|
79
|
+
};
|
80
|
+
});
|
81
|
+
}
|
82
|
+
|
83
|
+
public static async init(
|
84
|
+
host: string,
|
85
|
+
port: number,
|
86
|
+
token: string,
|
87
|
+
onMessageCB: OnMessageCallback,
|
88
|
+
onConnectionClosedCB: OnConnectionClosedCallback,
|
89
|
+
sessionId: string = "untitled",
|
90
|
+
agentProfileId: string | undefined = undefined
|
91
|
+
): Promise<FrontendChatClient> {
|
92
|
+
const params: Record<string, string> = { session_id: sessionId };
|
93
|
+
if (agentProfileId) {
|
94
|
+
params["agent_profile_id"] = agentProfileId;
|
95
|
+
}
|
96
|
+
return FrontendChatClient.initWithParams(
|
97
|
+
host,
|
98
|
+
port,
|
99
|
+
token,
|
100
|
+
params,
|
101
|
+
onMessageCB,
|
102
|
+
onConnectionClosedCB
|
103
|
+
);
|
104
|
+
}
|
105
|
+
|
106
|
+
public sendMessage(message: ClientToServer): void {
|
107
|
+
if (this.closed) {
|
108
|
+
throw new Error("Cannot send message on closed connection");
|
109
|
+
}
|
110
|
+
const data = JSON.stringify(message);
|
111
|
+
this.ws.send(data);
|
112
|
+
}
|
113
|
+
|
114
|
+
public close(): void {
|
115
|
+
this.closed = true;
|
116
|
+
this.onConnectionClosedCB();
|
117
|
+
this.ws.close();
|
118
|
+
}
|
119
|
+
|
120
|
+
public isClosed(): boolean {
|
121
|
+
return this.closed;
|
122
|
+
}
|
123
|
+
}
|