@xalia/agent 0.5.0 → 0.5.1
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 +46 -7
- package/dist/{agent.js → agent/src/agent/agent.js} +5 -4
- package/dist/{agentUtils.js → agent/src/agent/agentUtils.js} +10 -9
- package/dist/{mcpServerManager.js → agent/src/agent/mcpServerManager.js} +2 -1
- package/dist/{sudoMcpServerManager.js → agent/src/agent/sudoMcpServerManager.js} +4 -4
- package/dist/agent/src/chat/apiKeyManager.js +23 -0
- package/dist/agent/src/chat/asyncQueue.js +41 -0
- package/dist/agent/src/chat/client.js +126 -0
- package/dist/agent/src/chat/conversationManager.js +173 -0
- package/dist/agent/src/chat/db.js +186 -0
- package/dist/agent/src/chat/messages.js +2 -0
- package/dist/agent/src/chat/server.js +158 -0
- package/dist/agent/src/index.js +2 -0
- package/dist/agent/src/test/db.test.js +73 -0
- package/dist/{test → agent/src/test}/imageLoad.test.js +1 -1
- package/dist/{test → agent/src/test}/mcpServerManager.test.js +1 -1
- package/dist/{test → agent/src/test}/prompt.test.js +1 -1
- package/dist/{test → agent/src/test}/sudoMcpServerManager.test.js +3 -3
- package/dist/{chat.js → agent/src/tool/agentChat.js} +5 -5
- package/dist/{main.js → agent/src/tool/agentMain.js} +9 -15
- package/dist/agent/src/tool/chatMain.js +207 -0
- package/dist/agent/src/tool/main.js +54 -0
- package/dist/{options.js → agent/src/tool/options.js} +36 -2
- package/dist/agent/src/utils/asyncLock.js +45 -0
- package/dist/supabase/database.types.js +8 -0
- package/eslint.config.mjs +14 -14
- package/package.json +9 -15
- package/scripts/test_chat +84 -0
- package/src/{agent.ts → agent/agent.ts} +22 -11
- package/src/{agentUtils.ts → agent/agentUtils.ts} +13 -14
- package/src/{mcpServerManager.ts → agent/mcpServerManager.ts} +2 -1
- package/src/{sudoMcpServerManager.ts → agent/sudoMcpServerManager.ts} +3 -3
- package/src/chat/apiKeyManager.ts +24 -0
- package/src/chat/asyncQueue.ts +51 -0
- package/src/chat/client.ts +142 -0
- package/src/chat/conversationManager.ts +283 -0
- package/src/chat/db.ts +264 -0
- package/src/chat/messages.ts +91 -0
- package/src/chat/server.ts +177 -0
- package/src/test/db.test.ts +103 -0
- package/src/test/imageLoad.test.ts +1 -1
- package/src/test/mcpServerManager.test.ts +1 -1
- package/src/test/prompt.test.ts +1 -1
- package/src/test/sudoMcpServerManager.test.ts +6 -10
- package/src/{chat.ts → tool/agentChat.ts} +26 -24
- package/src/{main.ts → tool/agentMain.ts} +12 -19
- package/src/tool/chatMain.ts +250 -0
- package/src/{files.ts → tool/files.ts} +1 -1
- package/src/tool/main.ts +25 -0
- package/src/{nodePlatform.ts → tool/nodePlatform.ts} +1 -1
- package/src/{options.ts → tool/options.ts} +40 -1
- package/src/utils/asyncLock.ts +43 -0
- package/test_data/simplecalc_profile.json +1 -1
- package/test_data/sudomcp_import_profile.json +1 -1
- package/test_data/test_script_profile.json +1 -1
- package/tsconfig.json +1 -1
- package/scripts/test_script +0 -60
- /package/dist/{dummyLLM.js → agent/src/agent/dummyLLM.js} +0 -0
- /package/dist/{iplatform.js → agent/src/agent/iplatform.js} +0 -0
- /package/dist/{llm.js → agent/src/agent/llm.js} +0 -0
- /package/dist/{openAILLM.js → agent/src/agent/openAILLM.js} +0 -0
- /package/dist/{openAILLMStreaming.js → agent/src/agent/openAILLMStreaming.js} +0 -0
- /package/dist/{tokenAuth.js → agent/src/agent/tokenAuth.js} +0 -0
- /package/dist/{tools.js → agent/src/agent/tools.js} +0 -0
- /package/dist/{files.js → agent/src/tool/files.js} +0 -0
- /package/dist/{nodePlatform.js → agent/src/tool/nodePlatform.js} +0 -0
- /package/dist/{prompt.js → agent/src/tool/prompt.js} +0 -0
- /package/src/{dummyLLM.ts → agent/dummyLLM.ts} +0 -0
- /package/src/{iplatform.ts → agent/iplatform.ts} +0 -0
- /package/src/{llm.ts → agent/llm.ts} +0 -0
- /package/src/{openAILLM.ts → agent/openAILLM.ts} +0 -0
- /package/src/{openAILLMStreaming.ts → agent/openAILLMStreaming.ts} +0 -0
- /package/src/{tokenAuth.ts → agent/tokenAuth.ts} +0 -0
- /package/src/{tools.ts → agent/tools.ts} +0 -0
- /package/src/{test/prompt.test.src → index.ts} +0 -0
- /package/src/{prompt.ts → tool/prompt.ts} +0 -0
@@ -1,7 +1,7 @@
|
|
1
1
|
import { getLogger } from "@xalia/xmcp/sdk";
|
2
2
|
import { Agent, AgentProfile, OnMessageCB, OnToolCallCB } from "./agent";
|
3
3
|
import { IPlatform } from "./iplatform";
|
4
|
-
import {
|
4
|
+
import { SkillManager } from "./sudoMcpServerManager";
|
5
5
|
import OpenAI from "openai";
|
6
6
|
import { Configuration as SudoMcpConfiguration } from "@xalia/xmcp/sdk";
|
7
7
|
import { OpenAILLM } from "./openAILLM";
|
@@ -13,6 +13,7 @@ import { strict as assert } from "assert";
|
|
13
13
|
const logger = getLogger();
|
14
14
|
|
15
15
|
export const DEFAULT_LLM_URL = "http://localhost:5001/v1";
|
16
|
+
export const DEFAULT_LLM_MODEL = "gpt-4o";
|
16
17
|
|
17
18
|
/**
|
18
19
|
* Util function to create an Agent from some config information.
|
@@ -61,28 +62,28 @@ async function createAgent(
|
|
61
62
|
/**
|
62
63
|
* Util function to create and initialize an Agent given an AgentProfile.
|
63
64
|
*/
|
64
|
-
export async function
|
65
|
-
|
65
|
+
export async function createAgentWithSkills(
|
66
|
+
llmUrl: string,
|
66
67
|
agentProfile: AgentProfile,
|
67
68
|
onMessage: OnMessageCB,
|
68
69
|
onToolCall: OnToolCallCB,
|
69
70
|
platform: IPlatform,
|
70
|
-
|
71
|
+
llmApiKey: string | undefined,
|
71
72
|
sudomcpConfig: SudoMcpConfiguration,
|
72
73
|
authorizedUrl: string | undefined,
|
73
74
|
conversation: OpenAI.ChatCompletionMessageParam[] | undefined,
|
74
75
|
stream: boolean = false
|
75
|
-
): Promise<[Agent,
|
76
|
+
): Promise<[Agent, SkillManager]> {
|
76
77
|
// Create agent
|
77
78
|
logger.debug("[createAgentAndSudoMcpServerManager] creating agent ...");
|
78
79
|
const agent = await createAgent(
|
79
|
-
|
80
|
+
llmUrl,
|
80
81
|
agentProfile.model,
|
81
82
|
agentProfile.system_prompt,
|
82
83
|
onMessage,
|
83
84
|
onToolCall,
|
84
85
|
platform,
|
85
|
-
|
86
|
+
llmApiKey,
|
86
87
|
stream
|
87
88
|
);
|
88
89
|
if (conversation) {
|
@@ -90,10 +91,8 @@ export async function createAgentAndSudoMcpServerManager(
|
|
90
91
|
}
|
91
92
|
|
92
93
|
// Init SudoMcpServerManager
|
93
|
-
logger.debug(
|
94
|
-
|
95
|
-
);
|
96
|
-
const sudoMcpServerManager = await SudoMcpServerManager.initialize(
|
94
|
+
logger.debug("[createAgentWithSkills] creating SudoMcpServerManager.");
|
95
|
+
const sudoMcpServerManager = await SkillManager.initialize(
|
97
96
|
agent.getMcpServerManager(),
|
98
97
|
platform.openUrl,
|
99
98
|
sudomcpConfig.backend_url,
|
@@ -101,12 +100,12 @@ export async function createAgentAndSudoMcpServerManager(
|
|
101
100
|
authorizedUrl
|
102
101
|
);
|
103
102
|
logger.debug(
|
104
|
-
"[
|
103
|
+
"[createAgentWithSkills] restore mcp settings:" +
|
105
104
|
JSON.stringify(agentProfile.mcp_settings)
|
106
105
|
);
|
107
106
|
await sudoMcpServerManager.restoreMcpSettings(agentProfile.mcp_settings);
|
108
107
|
|
109
|
-
logger.debug("[
|
108
|
+
logger.debug("[createAgentWithSkills] done");
|
110
109
|
return [agent, sudoMcpServerManager];
|
111
110
|
}
|
112
111
|
|
@@ -135,7 +134,7 @@ export async function createNonInteractiveAgent(
|
|
135
134
|
return false;
|
136
135
|
};
|
137
136
|
|
138
|
-
const [agent, _] = await
|
137
|
+
const [agent, _] = await createAgentWithSkills(
|
139
138
|
url,
|
140
139
|
agentProfile,
|
141
140
|
onMessage,
|
@@ -177,7 +177,8 @@ export class McpServerManager {
|
|
177
177
|
try {
|
178
178
|
await client.connect(transport);
|
179
179
|
} catch (e) {
|
180
|
-
//
|
180
|
+
// Ensure the socket is closed so the process can exit if there is an
|
181
|
+
// error at connection time.
|
181
182
|
await client.close();
|
182
183
|
throw e;
|
183
184
|
}
|
@@ -39,7 +39,7 @@ class SanitizedServerBrief extends McpServerBrief {
|
|
39
39
|
* Manages access to the catalogue of servers hosted by sudomcp. Supports
|
40
40
|
* adding these servers to McpServerManager.
|
41
41
|
*/
|
42
|
-
export class
|
42
|
+
export class SkillManager {
|
43
43
|
private constructor(
|
44
44
|
private mcpServerManager: McpServerManager,
|
45
45
|
private apiClient: ApiClient,
|
@@ -69,7 +69,7 @@ export class SudoMcpServerManager {
|
|
69
69
|
sudoMcpUrl?: string,
|
70
70
|
sudoMcpApiKey?: string,
|
71
71
|
authorized_url?: string
|
72
|
-
): Promise<
|
72
|
+
): Promise<SkillManager> {
|
73
73
|
// TODO: Keep it on here and pass to `McpServerManager.addMcpServer`
|
74
74
|
const apiClient = new ApiClient(
|
75
75
|
sudoMcpUrl ?? DEFAULT_SERVER_URL,
|
@@ -79,7 +79,7 @@ export class SudoMcpServerManager {
|
|
79
79
|
const servers = await apiClient.listServers();
|
80
80
|
|
81
81
|
const [mcpServers, mcpServersMap] = buildServersList(servers);
|
82
|
-
return new
|
82
|
+
return new SkillManager(
|
83
83
|
mcpServerManager,
|
84
84
|
apiClient,
|
85
85
|
mcpServers,
|
@@ -0,0 +1,24 @@
|
|
1
|
+
// TODO:
|
2
|
+
// - lru-cache
|
3
|
+
|
4
|
+
import { Database, UserData } from "./db";
|
5
|
+
|
6
|
+
export class ApiKeyManager {
|
7
|
+
constructor(private db: Database) {}
|
8
|
+
|
9
|
+
public async verifyApiKey(apiKey: string): Promise<UserData | undefined> {
|
10
|
+
// TODO: Cache this
|
11
|
+
|
12
|
+
const userInfo = await this.db.getUserDataFromApiKey(apiKey);
|
13
|
+
return userInfo;
|
14
|
+
|
15
|
+
// if (apiKey.startsWith("dummy_key")) {
|
16
|
+
// return {
|
17
|
+
// user_uuid: apiKey,
|
18
|
+
// nickname: apiKey,
|
19
|
+
// };
|
20
|
+
// }
|
21
|
+
|
22
|
+
// return undefined;
|
23
|
+
}
|
24
|
+
}
|
@@ -0,0 +1,51 @@
|
|
1
|
+
export class AsyncQueue<T> {
|
2
|
+
private queue: T[];
|
3
|
+
private process: (queueEntry: T) => Promise<void>;
|
4
|
+
private maxBacklog: number;
|
5
|
+
private running: boolean = false;
|
6
|
+
|
7
|
+
constructor(
|
8
|
+
process: (queueEntry: T) => Promise<void>,
|
9
|
+
maxBacklog: number = 100
|
10
|
+
) {
|
11
|
+
this.queue = [];
|
12
|
+
this.process = process;
|
13
|
+
this.maxBacklog = maxBacklog;
|
14
|
+
}
|
15
|
+
|
16
|
+
public getLength(): number {
|
17
|
+
return this.queue.length;
|
18
|
+
}
|
19
|
+
|
20
|
+
public getMaxBacklog(): number {
|
21
|
+
return this.maxBacklog;
|
22
|
+
}
|
23
|
+
|
24
|
+
public async enqueueAsync(queueEntry: T): Promise<void> {
|
25
|
+
while (this.maxBacklog > 0 && this.queue.length >= this.maxBacklog) {
|
26
|
+
await new Promise((r) => setTimeout(r, 1));
|
27
|
+
}
|
28
|
+
this.queue.push(queueEntry);
|
29
|
+
this.tryNext();
|
30
|
+
}
|
31
|
+
|
32
|
+
private shift(): T | undefined {
|
33
|
+
return this.queue.shift();
|
34
|
+
}
|
35
|
+
|
36
|
+
private async tryNext(): Promise<void> {
|
37
|
+
if (this.running) {
|
38
|
+
return;
|
39
|
+
}
|
40
|
+
|
41
|
+
const queueEntry = this.shift();
|
42
|
+
if (queueEntry) {
|
43
|
+
this.running = true;
|
44
|
+
await this.process(queueEntry);
|
45
|
+
this.running = false;
|
46
|
+
|
47
|
+
// Check for more tasks on the queue.
|
48
|
+
setTimeout(() => this.tryNext());
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
@@ -0,0 +1,142 @@
|
|
1
|
+
import * as dotenv from "dotenv";
|
2
|
+
import { getLogger } from "@xalia/xmcp/sdk";
|
3
|
+
import { strict as assert } from "assert";
|
4
|
+
import * as websocket from "ws";
|
5
|
+
import { ServerToClient, ClientToServer } from "./messages";
|
6
|
+
|
7
|
+
dotenv.config();
|
8
|
+
|
9
|
+
const logger = getLogger();
|
10
|
+
|
11
|
+
type OnMessageCallback = (msg: ServerToClient) => void;
|
12
|
+
|
13
|
+
type OnConnectionClosedCallback = () => void;
|
14
|
+
|
15
|
+
export class ChatClient {
|
16
|
+
private ws: websocket.WebSocket;
|
17
|
+
private onMessageCB: OnMessageCallback;
|
18
|
+
private onConnectionClosedCB: OnConnectionClosedCallback;
|
19
|
+
private closed: boolean;
|
20
|
+
|
21
|
+
private constructor(
|
22
|
+
ws: websocket.WebSocket,
|
23
|
+
onMessageCB: OnMessageCallback,
|
24
|
+
onConnectionClosedCB: OnConnectionClosedCallback
|
25
|
+
) {
|
26
|
+
this.ws = ws;
|
27
|
+
this.onMessageCB = onMessageCB;
|
28
|
+
this.onConnectionClosedCB = onConnectionClosedCB;
|
29
|
+
this.closed = false;
|
30
|
+
}
|
31
|
+
|
32
|
+
static async initWithParams(
|
33
|
+
host: string,
|
34
|
+
port: number,
|
35
|
+
token: string,
|
36
|
+
params: Record<string, string>,
|
37
|
+
onMessageCB: OnMessageCallback,
|
38
|
+
onConnectionClosedCB: OnConnectionClosedCallback
|
39
|
+
): Promise<ChatClient> {
|
40
|
+
return new Promise((r, e) => {
|
41
|
+
const urlParams = new URLSearchParams(params);
|
42
|
+
const url = `ws://${host}:${port}?${urlParams}`;
|
43
|
+
|
44
|
+
const ws = new websocket.WebSocket(url, [token]);
|
45
|
+
logger.info("created ws");
|
46
|
+
|
47
|
+
const client = new ChatClient(ws, onMessageCB, onConnectionClosedCB);
|
48
|
+
|
49
|
+
ws.onopen = async () => {
|
50
|
+
logger.info("opened");
|
51
|
+
|
52
|
+
ws.onmessage = (ev: websocket.MessageEvent) => {
|
53
|
+
try {
|
54
|
+
const msgData = ev.data;
|
55
|
+
if (typeof msgData !== "string") {
|
56
|
+
throw `expected "string" data, got ${typeof msgData}`;
|
57
|
+
}
|
58
|
+
logger.debug(`[client.onmessage]: ${msgData}`);
|
59
|
+
const msg: ServerToClient = JSON.parse(msgData);
|
60
|
+
client.onMessageCB(msg);
|
61
|
+
} catch (e) {
|
62
|
+
client.close();
|
63
|
+
throw e;
|
64
|
+
}
|
65
|
+
};
|
66
|
+
r(client);
|
67
|
+
};
|
68
|
+
|
69
|
+
ws.onclose = (err) => {
|
70
|
+
logger.info("closed");
|
71
|
+
logger.info(
|
72
|
+
`[client] WebSocket connection closed: ${JSON.stringify(err)}`
|
73
|
+
);
|
74
|
+
client.closed = true;
|
75
|
+
onConnectionClosedCB();
|
76
|
+
};
|
77
|
+
|
78
|
+
ws.onerror = (error) => {
|
79
|
+
logger.error("[client] WebSocket error:", JSON.stringify(error));
|
80
|
+
e(error);
|
81
|
+
|
82
|
+
client.closed = true;
|
83
|
+
onConnectionClosedCB();
|
84
|
+
};
|
85
|
+
});
|
86
|
+
}
|
87
|
+
|
88
|
+
// public static async initWithSession(
|
89
|
+
// host: string,
|
90
|
+
// port: number,
|
91
|
+
// token: string,
|
92
|
+
// sessionId: string,
|
93
|
+
// onMessageCB: OnMessageCallback,
|
94
|
+
// onConnectionClosedCB: OnConnectionClosedCallback
|
95
|
+
// ): Promise<ChatClient> {
|
96
|
+
// return ChatClient.initWithParams(
|
97
|
+
// host,
|
98
|
+
// port,
|
99
|
+
// token,
|
100
|
+
// { session_id: sessionId },
|
101
|
+
// onMessageCB,
|
102
|
+
// onConnectionClosedCB
|
103
|
+
// );
|
104
|
+
// }
|
105
|
+
|
106
|
+
public static async init(
|
107
|
+
host: string,
|
108
|
+
port: number,
|
109
|
+
token: string,
|
110
|
+
onMessageCB: OnMessageCallback,
|
111
|
+
onConnectionClosedCB: OnConnectionClosedCallback,
|
112
|
+
sessionId: string = "untitled",
|
113
|
+
agentProfileId: string | undefined = undefined
|
114
|
+
): Promise<ChatClient> {
|
115
|
+
const params: Record<string, string> = { session_id: sessionId };
|
116
|
+
if (agentProfileId) {
|
117
|
+
params["agent_profile_id"] = agentProfileId;
|
118
|
+
}
|
119
|
+
return ChatClient.initWithParams(
|
120
|
+
host,
|
121
|
+
port,
|
122
|
+
token,
|
123
|
+
params,
|
124
|
+
onMessageCB,
|
125
|
+
onConnectionClosedCB
|
126
|
+
);
|
127
|
+
}
|
128
|
+
|
129
|
+
public sendMessage(message: ClientToServer): void {
|
130
|
+
assert(!this.closed);
|
131
|
+
const data = JSON.stringify(message);
|
132
|
+
this.ws.send(data);
|
133
|
+
}
|
134
|
+
|
135
|
+
public close(): void {
|
136
|
+
this.closed = true;
|
137
|
+
this.onConnectionClosedCB();
|
138
|
+
this.ws.close();
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
// TODO: remove this
|
@@ -0,0 +1,283 @@
|
|
1
|
+
import * as ws from "ws";
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
3
|
+
import { strict as assert } from "assert";
|
4
|
+
|
5
|
+
import { Configuration, getLogger } from "@xalia/xmcp/sdk";
|
6
|
+
|
7
|
+
import {
|
8
|
+
Agent,
|
9
|
+
ChatCompletionMessageParam,
|
10
|
+
ChatCompletionMessageToolCall,
|
11
|
+
} from "../agent/agent";
|
12
|
+
import { SkillManager } from "../agent/sudoMcpServerManager";
|
13
|
+
import { createAgentWithSkills } from "../agent/agentUtils";
|
14
|
+
|
15
|
+
import type {
|
16
|
+
ClientToServer,
|
17
|
+
ServerToClient,
|
18
|
+
ClientUserMessage,
|
19
|
+
} from "./messages";
|
20
|
+
import { AsyncQueue } from "./asyncQueue";
|
21
|
+
import { Database, UserData } from "./db";
|
22
|
+
import { AsyncLock } from "../utils/asyncLock";
|
23
|
+
|
24
|
+
const logger = getLogger();
|
25
|
+
|
26
|
+
export class UserAlreadyConnected extends Error {
|
27
|
+
constructor() {
|
28
|
+
super("User already connected to the conversation");
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
type QueuedClientMessage<T extends ClientToServer = ClientToServer> = {
|
33
|
+
msg: T;
|
34
|
+
from: string;
|
35
|
+
};
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Describes a Session (conversation) with connected participants.
|
39
|
+
*/
|
40
|
+
export class OpenSession {
|
41
|
+
public onEmpty: () => void;
|
42
|
+
public connections: Record<string, ws.WebSocket>;
|
43
|
+
public agent: Agent;
|
44
|
+
public skillManager: SkillManager;
|
45
|
+
public messageQueue: AsyncQueue<QueuedClientMessage>;
|
46
|
+
public curAgentMsgId: string | undefined;
|
47
|
+
|
48
|
+
constructor(
|
49
|
+
agent: Agent,
|
50
|
+
sudoMcpServerManager: SkillManager,
|
51
|
+
onEmpty: () => void
|
52
|
+
) {
|
53
|
+
// public agent: Agent,
|
54
|
+
this.agent = agent;
|
55
|
+
this.skillManager = sudoMcpServerManager;
|
56
|
+
this.onEmpty = onEmpty;
|
57
|
+
this.connections = {};
|
58
|
+
this.messageQueue = new AsyncQueue<QueuedClientMessage>((m) =>
|
59
|
+
this.onMessage(m)
|
60
|
+
);
|
61
|
+
this.curAgentMsgId = undefined;
|
62
|
+
}
|
63
|
+
|
64
|
+
join(userName: string, ws: ws.WebSocket): void {
|
65
|
+
if (this.connections[userName]) {
|
66
|
+
throw new UserAlreadyConnected();
|
67
|
+
}
|
68
|
+
|
69
|
+
// Inform any other participants, and add the WebSocket to the map.
|
70
|
+
|
71
|
+
this.broadcast({ type: "user_joined", user: userName });
|
72
|
+
this.connections[userName] = ws;
|
73
|
+
|
74
|
+
ws.on("message", async (message: ws.RawData) => {
|
75
|
+
logger.debug(`[convMgr]: got message: (from ${userName}): ${message}`);
|
76
|
+
const msgStr = message.toString();
|
77
|
+
const msg: ClientToServer = JSON.parse(msgStr);
|
78
|
+
await this.messageQueue.enqueueAsync({ msg, from: userName });
|
79
|
+
});
|
80
|
+
|
81
|
+
ws.on("close", () => {
|
82
|
+
logger.debug(`[convMgr]: ${userName} closed`);
|
83
|
+
|
84
|
+
// Remove our connection then inform any other participants
|
85
|
+
|
86
|
+
delete this.connections[userName];
|
87
|
+
this.broadcast({ type: "user_left", user: userName });
|
88
|
+
|
89
|
+
if (Object.keys(this.connections).length == 0) {
|
90
|
+
this.onEmpty();
|
91
|
+
}
|
92
|
+
});
|
93
|
+
}
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Called once for each message. Messages are queued to avoid overlapping
|
97
|
+
* calls to the LLM.
|
98
|
+
*/
|
99
|
+
async onMessage(queuedMessage: QueuedClientMessage): Promise<void> {
|
100
|
+
logger.debug(
|
101
|
+
`[onMessage]: processing (${queuedMessage.from}) ` +
|
102
|
+
`${JSON.stringify(queuedMessage.msg)}`
|
103
|
+
);
|
104
|
+
|
105
|
+
const msg = queuedMessage.msg;
|
106
|
+
switch (msg.type) {
|
107
|
+
case "msg":
|
108
|
+
return this.onChatMessage(
|
109
|
+
queuedMessage as QueuedClientMessage<ClientUserMessage>
|
110
|
+
);
|
111
|
+
default:
|
112
|
+
throw `unknown message: ${JSON.stringify(queuedMessage)}`;
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
async onAgentMessage(msg: string, end: boolean): Promise<void> {
|
117
|
+
logger.debug(`[onAgentMessage] msg: ${msg}, end: ${end}`);
|
118
|
+
assert(this.curAgentMsgId);
|
119
|
+
this.broadcast({
|
120
|
+
type: "agent_msg_chunk",
|
121
|
+
message_id: this.curAgentMsgId,
|
122
|
+
message: msg,
|
123
|
+
end,
|
124
|
+
});
|
125
|
+
}
|
126
|
+
|
127
|
+
async onChatMessage(
|
128
|
+
queuedMessage: QueuedClientMessage<ClientUserMessage>
|
129
|
+
): Promise<void> {
|
130
|
+
const msg = queuedMessage.msg;
|
131
|
+
const userToken = queuedMessage.from;
|
132
|
+
const msgId = uuidv4();
|
133
|
+
this.broadcast({
|
134
|
+
type: "user_msg",
|
135
|
+
message_id: msgId,
|
136
|
+
message: msg.message,
|
137
|
+
from: userToken,
|
138
|
+
});
|
139
|
+
|
140
|
+
// Messages will be handled by the Agent.onMessage callback. We await the
|
141
|
+
// response here before processing further messages.
|
142
|
+
|
143
|
+
this.curAgentMsgId = `${msgId}-resp`;
|
144
|
+
await this.agent.userMessage(msg.message, undefined, userToken);
|
145
|
+
}
|
146
|
+
|
147
|
+
broadcast(msg: ServerToClient) {
|
148
|
+
logger.info(`[broadcast]: broadcast msg: ${JSON.stringify(msg)}`);
|
149
|
+
const msgString = JSON.stringify(msg);
|
150
|
+
for (const ws of Object.values(this.connections)) {
|
151
|
+
ws.send(msgString);
|
152
|
+
}
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
/**
|
157
|
+
* Handles forwarding of messages between all participants of a session, as
|
158
|
+
* well as messages to/from and Agent.
|
159
|
+
*/
|
160
|
+
export class ConversationManager {
|
161
|
+
public db: Database;
|
162
|
+
public llmUrl: string;
|
163
|
+
public xmcpUrl: string;
|
164
|
+
public openSessionsLock: AsyncLock;
|
165
|
+
public openSessions: Record<string, OpenSession>;
|
166
|
+
|
167
|
+
constructor(db: Database, llmUrl: string, xmcpUrl: string) {
|
168
|
+
this.db = db;
|
169
|
+
this.llmUrl = llmUrl;
|
170
|
+
this.xmcpUrl = xmcpUrl;
|
171
|
+
this.openSessionsLock = new AsyncLock();
|
172
|
+
this.openSessions = {};
|
173
|
+
}
|
174
|
+
|
175
|
+
async join(
|
176
|
+
sessionId: string,
|
177
|
+
llmApiKey: string,
|
178
|
+
xmcpApiKey: string,
|
179
|
+
userData: UserData,
|
180
|
+
ws: ws.WebSocket
|
181
|
+
): Promise<void> {
|
182
|
+
await this.openSessionsLock.lockAndProcess(() => {
|
183
|
+
return this.getOrCreateAndSubscribe(
|
184
|
+
sessionId,
|
185
|
+
llmApiKey,
|
186
|
+
xmcpApiKey,
|
187
|
+
userData.nickname || userData.user_uuid,
|
188
|
+
ws
|
189
|
+
);
|
190
|
+
});
|
191
|
+
}
|
192
|
+
/**
|
193
|
+
* Must be called while holding the openSessionsLock
|
194
|
+
*/
|
195
|
+
private async getOrCreateAndSubscribe(
|
196
|
+
sessionId: string,
|
197
|
+
llmApiKey: string,
|
198
|
+
xmcpApiKey: string,
|
199
|
+
userName: string,
|
200
|
+
ws: ws.WebSocket
|
201
|
+
): Promise<OpenSession> {
|
202
|
+
let openSession = this.openSessions[sessionId];
|
203
|
+
if (openSession) {
|
204
|
+
openSession.join(userName, ws);
|
205
|
+
return openSession;
|
206
|
+
}
|
207
|
+
|
208
|
+
// Create a new session
|
209
|
+
|
210
|
+
// TODO: The owner of llmApiKey and xmcpApiKey may not be the owner of the
|
211
|
+
// session. Should we create the Agent and SudoMcpServerManager with the
|
212
|
+
// session-owners api key?
|
213
|
+
|
214
|
+
const onEmpty = () => {
|
215
|
+
logger.debug(`session ${sessionId} empty. removing`);
|
216
|
+
delete this.openSessions[sessionId];
|
217
|
+
};
|
218
|
+
|
219
|
+
const sessionData = await this.db.getSessionById(sessionId);
|
220
|
+
if (!sessionData) {
|
221
|
+
throw `no such session ${sessionId}`;
|
222
|
+
}
|
223
|
+
const conversation =
|
224
|
+
sessionData.conversation as unknown as ChatCompletionMessageParam[];
|
225
|
+
|
226
|
+
const agentProfile = await this.db.getAgentProfileById(
|
227
|
+
sessionData.agent_profile_uuid
|
228
|
+
);
|
229
|
+
if (!agentProfile) {
|
230
|
+
throw `no such agent profile ${sessionData.agent_profile_uuid}`;
|
231
|
+
}
|
232
|
+
|
233
|
+
const platform = {
|
234
|
+
openUrl: (
|
235
|
+
_url: string,
|
236
|
+
_authResultP: Promise<boolean>,
|
237
|
+
_displayName: string
|
238
|
+
) => {
|
239
|
+
throw "unimpl";
|
240
|
+
},
|
241
|
+
load: (_filename: string): Promise<string> => {
|
242
|
+
throw "unimpl";
|
243
|
+
},
|
244
|
+
};
|
245
|
+
|
246
|
+
// Forward agent messages to the OpenSession (once it is iniailized
|
247
|
+
|
248
|
+
const context: { openSession?: OpenSession } = {};
|
249
|
+
const onMessage = async (msg: string, end: boolean) => {
|
250
|
+
logger.debug(`[onMessage] msg: ${msg}, end: ${end}`);
|
251
|
+
assert(context.openSession);
|
252
|
+
context.openSession.onAgentMessage(msg, end);
|
253
|
+
};
|
254
|
+
|
255
|
+
const onToolCall = async (
|
256
|
+
toolCall: ChatCompletionMessageToolCall
|
257
|
+
): Promise<boolean> => {
|
258
|
+
logger.debug(`[onToolCall] : ${JSON.stringify(toolCall)}`);
|
259
|
+
return true;
|
260
|
+
};
|
261
|
+
|
262
|
+
const xmcpConfig = Configuration.new(xmcpApiKey, this.xmcpUrl, false);
|
263
|
+
|
264
|
+
const [agent, smsm] = await createAgentWithSkills(
|
265
|
+
this.llmUrl,
|
266
|
+
agentProfile,
|
267
|
+
onMessage,
|
268
|
+
onToolCall,
|
269
|
+
platform,
|
270
|
+
llmApiKey,
|
271
|
+
xmcpConfig,
|
272
|
+
undefined,
|
273
|
+
conversation,
|
274
|
+
true
|
275
|
+
);
|
276
|
+
|
277
|
+
openSession = new OpenSession(agent, smsm, onEmpty);
|
278
|
+
context.openSession = openSession;
|
279
|
+
this.openSessions[sessionId] = openSession;
|
280
|
+
openSession.join(userName, ws);
|
281
|
+
return openSession;
|
282
|
+
}
|
283
|
+
}
|