@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
package/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Sudobase Agent
|
2
2
|
|
3
|
-
## Setup
|
3
|
+
## Agent Setup
|
4
4
|
|
5
5
|
```sh
|
6
6
|
# In the root folder
|
@@ -11,46 +11,85 @@ yarn workspaces run build
|
|
11
11
|
Run a local backend server (follow instructions in `mcppro`) because authentication against deployed backend is WIP.
|
12
12
|
|
13
13
|
## Usage:
|
14
|
+
|
14
15
|
To enter a chat with no initial prompt and default system prompt:
|
16
|
+
|
15
17
|
```sh
|
16
18
|
node dist/main.js
|
17
19
|
```
|
18
20
|
|
19
21
|
Optional arguments are `prompt` (first User message) and `systemprompt`
|
22
|
+
|
20
23
|
```sh
|
21
24
|
node dist/main.js --prompt 'Who is the new pope?' --sysprompt 'You are extremely polite.'
|
22
25
|
```
|
23
26
|
|
24
27
|
## Features:
|
28
|
+
|
25
29
|
### Conversation:
|
30
|
+
|
26
31
|
CLI-mode is a conversation between user and LLM.
|
27
32
|
|
28
33
|
### Tool selection:
|
34
|
+
|
29
35
|
We now support MCP tool calls. Currently servers are enabled by editing the `mcpServerUrls.json` file, but this will be improved soon.
|
30
36
|
|
31
37
|
### Model selection:
|
38
|
+
|
32
39
|
The CLI uses the default model (`gpt-4o-mini`) but uncomment the `agent.chooseModel` line to switch to `gpt-4.1-2025-04-14`. Right now we can use any OpenAI model that supports tool calling.
|
33
40
|
|
34
41
|
Supporting inference providers like Together.ai is TODO.
|
35
42
|
|
36
43
|
### Callbacks
|
44
|
+
|
37
45
|
The CLI uses an `onMessage` callback to display the Agent's messages and an `onToolCall` callback to request authorization for tool calls.
|
38
46
|
|
39
47
|
## Development Notes
|
40
48
|
|
41
49
|
### Architecture
|
50
|
+
|
42
51
|
Frontend talks to
|
43
|
-
|
44
|
-
|
45
|
-
|
52
|
+
|
53
|
+
- Agent (for conversation, ChatCompletion)
|
54
|
+
- McpServerManager (to enable, disable tools that have been added)
|
55
|
+
- SudoMcpServerManager (to access catalog of SudoMCP servers, add to McpServerManager)
|
46
56
|
|
47
57
|
SudoMcpServerManager:
|
48
|
-
|
49
|
-
|
50
|
-
|
58
|
+
|
59
|
+
- track list of available mcp servers (via sdk/ApiClient)
|
60
|
+
- get the list of tools as required by UI (via sdk/ApiClient)
|
61
|
+
- add tools to McpServerManager
|
51
62
|
|
52
63
|
McpServerManager:
|
53
64
|
- manager (mcpServer, tool)
|
54
65
|
- enabling / disabling
|
55
66
|
- list of enabled / available tools per mcp server
|
56
67
|
- exposes tools to Agent
|
68
|
+
|
69
|
+
## Multi-agent Chat Setup
|
70
|
+
|
71
|
+
With the mcppro backend and DB running locally:
|
72
|
+
|
73
|
+
```
|
74
|
+
$ ./scripts/test_chat
|
75
|
+
```
|
76
|
+
|
77
|
+
to set up some users.
|
78
|
+
|
79
|
+
```
|
80
|
+
$ echo "LLM_URL=http://localhost:5001/v1" >> .env
|
81
|
+
$ echo "LLM_API_KEY=dummy_key" >> .env
|
82
|
+
$ ./dist/agent/src/tool/main.js chat server
|
83
|
+
```
|
84
|
+
|
85
|
+
(in 2 other terminals)
|
86
|
+
|
87
|
+
Join as chatuser0:
|
88
|
+
```
|
89
|
+
$ ./dist/agent/src/tool/main.js chat client --api-key `cat _test_chat/chatuser0.apikey` --session test_session
|
90
|
+
```
|
91
|
+
|
92
|
+
Join as chatuser1:
|
93
|
+
```
|
94
|
+
$ ./dist/agent/src/tool/main.js chat client --api-key `cat _test_chat/chatuser1.apikey` --session chatuser0/test_session
|
95
|
+
```
|
@@ -79,15 +79,15 @@ class Agent {
|
|
79
79
|
}
|
80
80
|
setConversation(messages) {
|
81
81
|
(0, assert_1.strict)(this.messages[0].role == "system");
|
82
|
-
(0, assert_1.strict)(messages[0].role != "system", "conversation contains system msg");
|
82
|
+
(0, assert_1.strict)(messages.length === 0 || messages[0].role != "system", "conversation contains system msg");
|
83
83
|
const newMessages = [this.messages[0]];
|
84
84
|
this.messages = newMessages.concat(structuredClone(messages));
|
85
85
|
}
|
86
86
|
getMcpServerManager() {
|
87
87
|
return this.mcpServerManager;
|
88
88
|
}
|
89
|
-
async userMessage(msg, imageB64) {
|
90
|
-
const userMessage = createUserMessage(msg, imageB64);
|
89
|
+
async userMessage(msg, imageB64, name) {
|
90
|
+
const userMessage = createUserMessage(msg, imageB64, name);
|
91
91
|
if (!userMessage) {
|
92
92
|
return undefined;
|
93
93
|
}
|
@@ -203,7 +203,7 @@ exports.Agent = Agent;
|
|
203
203
|
* (optional) image. If neither is given (null message), then undefined is
|
204
204
|
* returned.
|
205
205
|
**/
|
206
|
-
function createUserMessage(msg, imageB64) {
|
206
|
+
function createUserMessage(msg, imageB64, name) {
|
207
207
|
const content = (() => {
|
208
208
|
if (!imageB64) {
|
209
209
|
if (!msg) {
|
@@ -234,5 +234,6 @@ function createUserMessage(msg, imageB64) {
|
|
234
234
|
return {
|
235
235
|
role: "user",
|
236
236
|
content,
|
237
|
+
name,
|
237
238
|
};
|
238
239
|
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.DEFAULT_LLM_URL = void 0;
|
4
|
-
exports.
|
3
|
+
exports.DEFAULT_LLM_MODEL = exports.DEFAULT_LLM_URL = void 0;
|
4
|
+
exports.createAgentWithSkills = createAgentWithSkills;
|
5
5
|
exports.createNonInteractiveAgent = createNonInteractiveAgent;
|
6
6
|
exports.runOneShot = runOneShot;
|
7
7
|
const sdk_1 = require("@xalia/xmcp/sdk");
|
@@ -13,6 +13,7 @@ const dummyLLM_1 = require("./dummyLLM");
|
|
13
13
|
const assert_1 = require("assert");
|
14
14
|
const logger = (0, sdk_1.getLogger)();
|
15
15
|
exports.DEFAULT_LLM_URL = "http://localhost:5001/v1";
|
16
|
+
exports.DEFAULT_LLM_MODEL = "gpt-4o";
|
16
17
|
/**
|
17
18
|
* Util function to create an Agent from some config information.
|
18
19
|
*/
|
@@ -49,20 +50,20 @@ async function createAgent(llmUrl, model, systemPrompt, onMessage, onToolCall, p
|
|
49
50
|
/**
|
50
51
|
* Util function to create and initialize an Agent given an AgentProfile.
|
51
52
|
*/
|
52
|
-
async function
|
53
|
+
async function createAgentWithSkills(llmUrl, agentProfile, onMessage, onToolCall, platform, llmApiKey, sudomcpConfig, authorizedUrl, conversation, stream = false) {
|
53
54
|
// Create agent
|
54
55
|
logger.debug("[createAgentAndSudoMcpServerManager] creating agent ...");
|
55
|
-
const agent = await createAgent(
|
56
|
+
const agent = await createAgent(llmUrl, agentProfile.model, agentProfile.system_prompt, onMessage, onToolCall, platform, llmApiKey, stream);
|
56
57
|
if (conversation) {
|
57
58
|
agent.setConversation(conversation);
|
58
59
|
}
|
59
60
|
// Init SudoMcpServerManager
|
60
|
-
logger.debug("[
|
61
|
-
const sudoMcpServerManager = await sudoMcpServerManager_1.
|
62
|
-
logger.debug("[
|
61
|
+
logger.debug("[createAgentWithSkills] creating SudoMcpServerManager.");
|
62
|
+
const sudoMcpServerManager = await sudoMcpServerManager_1.SkillManager.initialize(agent.getMcpServerManager(), platform.openUrl, sudomcpConfig.backend_url, sudomcpConfig.api_key, authorizedUrl);
|
63
|
+
logger.debug("[createAgentWithSkills] restore mcp settings:" +
|
63
64
|
JSON.stringify(agentProfile.mcp_settings));
|
64
65
|
await sudoMcpServerManager.restoreMcpSettings(agentProfile.mcp_settings);
|
65
|
-
logger.debug("[
|
66
|
+
logger.debug("[createAgentWithSkills] done");
|
66
67
|
return [agent, sudoMcpServerManager];
|
67
68
|
}
|
68
69
|
/**
|
@@ -81,7 +82,7 @@ async function createNonInteractiveAgent(url, agentProfile, conversation, platfo
|
|
81
82
|
}
|
82
83
|
return false;
|
83
84
|
};
|
84
|
-
const [agent, _] = await
|
85
|
+
const [agent, _] = await createAgentWithSkills(url, agentProfile, onMessage, onToolCall, platform, openaiApiKey, sudomcpConfig, undefined, conversation);
|
85
86
|
return agent;
|
86
87
|
}
|
87
88
|
/**
|
@@ -127,7 +127,8 @@ class McpServerManager {
|
|
127
127
|
await client.connect(transport);
|
128
128
|
}
|
129
129
|
catch (e) {
|
130
|
-
//
|
130
|
+
// Ensure the socket is closed so the process can exit if there is an
|
131
|
+
// error at connection time.
|
131
132
|
await client.close();
|
132
133
|
throw e;
|
133
134
|
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.
|
3
|
+
exports.SkillManager = exports.LOCAL_SERVER_URL = void 0;
|
4
4
|
const sdk_1 = require("@xalia/xmcp/sdk");
|
5
5
|
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
6
6
|
const logger = (0, sdk_1.getLogger)();
|
@@ -25,7 +25,7 @@ class SanitizedServerBrief extends sdk_1.McpServerBrief {
|
|
25
25
|
* Manages access to the catalogue of servers hosted by sudomcp. Supports
|
26
26
|
* adding these servers to McpServerManager.
|
27
27
|
*/
|
28
|
-
class
|
28
|
+
class SkillManager {
|
29
29
|
constructor(mcpServerManager, apiClient, serverBriefs, serverBriefsMap, toolCache, openUrl,
|
30
30
|
// Redirect to this page after successful authorization
|
31
31
|
authorized_url) {
|
@@ -47,7 +47,7 @@ class SudoMcpServerManager {
|
|
47
47
|
// Fetch server list
|
48
48
|
const servers = await apiClient.listServers();
|
49
49
|
const [mcpServers, mcpServersMap] = buildServersList(servers);
|
50
|
-
return new
|
50
|
+
return new SkillManager(mcpServerManager, apiClient, mcpServers, mcpServersMap, {}, openUrl, authorized_url);
|
51
51
|
}
|
52
52
|
/// TODO: Bit awkward that we have to restore via this class, but it's the
|
53
53
|
/// only class which knows how to restore (restart) the mcp servers.
|
@@ -138,7 +138,7 @@ class SudoMcpServerManager {
|
|
138
138
|
return this.apiClient;
|
139
139
|
}
|
140
140
|
}
|
141
|
-
exports.
|
141
|
+
exports.SkillManager = SkillManager;
|
142
142
|
/**
|
143
143
|
* Connect a client to a hosted MCP server session,
|
144
144
|
* prompting for authentication if needed.
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"use strict";
|
2
|
+
// TODO:
|
3
|
+
// - lru-cache
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
5
|
+
exports.ApiKeyManager = void 0;
|
6
|
+
class ApiKeyManager {
|
7
|
+
constructor(db) {
|
8
|
+
this.db = db;
|
9
|
+
}
|
10
|
+
async verifyApiKey(apiKey) {
|
11
|
+
// TODO: Cache this
|
12
|
+
const userInfo = await this.db.getUserDataFromApiKey(apiKey);
|
13
|
+
return userInfo;
|
14
|
+
// if (apiKey.startsWith("dummy_key")) {
|
15
|
+
// return {
|
16
|
+
// user_uuid: apiKey,
|
17
|
+
// nickname: apiKey,
|
18
|
+
// };
|
19
|
+
// }
|
20
|
+
// return undefined;
|
21
|
+
}
|
22
|
+
}
|
23
|
+
exports.ApiKeyManager = ApiKeyManager;
|
@@ -0,0 +1,41 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.AsyncQueue = void 0;
|
4
|
+
class AsyncQueue {
|
5
|
+
constructor(process, maxBacklog = 100) {
|
6
|
+
this.running = false;
|
7
|
+
this.queue = [];
|
8
|
+
this.process = process;
|
9
|
+
this.maxBacklog = maxBacklog;
|
10
|
+
}
|
11
|
+
getLength() {
|
12
|
+
return this.queue.length;
|
13
|
+
}
|
14
|
+
getMaxBacklog() {
|
15
|
+
return this.maxBacklog;
|
16
|
+
}
|
17
|
+
async enqueueAsync(queueEntry) {
|
18
|
+
while (this.maxBacklog > 0 && this.queue.length >= this.maxBacklog) {
|
19
|
+
await new Promise((r) => setTimeout(r, 1));
|
20
|
+
}
|
21
|
+
this.queue.push(queueEntry);
|
22
|
+
this.tryNext();
|
23
|
+
}
|
24
|
+
shift() {
|
25
|
+
return this.queue.shift();
|
26
|
+
}
|
27
|
+
async tryNext() {
|
28
|
+
if (this.running) {
|
29
|
+
return;
|
30
|
+
}
|
31
|
+
const queueEntry = this.shift();
|
32
|
+
if (queueEntry) {
|
33
|
+
this.running = true;
|
34
|
+
await this.process(queueEntry);
|
35
|
+
this.running = false;
|
36
|
+
// Check for more tasks on the queue.
|
37
|
+
setTimeout(() => this.tryNext());
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
exports.AsyncQueue = AsyncQueue;
|
@@ -0,0 +1,126 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
19
|
+
var ownKeys = function(o) {
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
21
|
+
var ar = [];
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
23
|
+
return ar;
|
24
|
+
};
|
25
|
+
return ownKeys(o);
|
26
|
+
};
|
27
|
+
return function (mod) {
|
28
|
+
if (mod && mod.__esModule) return mod;
|
29
|
+
var result = {};
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
31
|
+
__setModuleDefault(result, mod);
|
32
|
+
return result;
|
33
|
+
};
|
34
|
+
})();
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
36
|
+
exports.ChatClient = void 0;
|
37
|
+
const dotenv = __importStar(require("dotenv"));
|
38
|
+
const sdk_1 = require("@xalia/xmcp/sdk");
|
39
|
+
const assert_1 = require("assert");
|
40
|
+
const websocket = __importStar(require("ws"));
|
41
|
+
dotenv.config();
|
42
|
+
const logger = (0, sdk_1.getLogger)();
|
43
|
+
class ChatClient {
|
44
|
+
constructor(ws, onMessageCB, onConnectionClosedCB) {
|
45
|
+
this.ws = ws;
|
46
|
+
this.onMessageCB = onMessageCB;
|
47
|
+
this.onConnectionClosedCB = onConnectionClosedCB;
|
48
|
+
this.closed = false;
|
49
|
+
}
|
50
|
+
static async initWithParams(host, port, token, params, onMessageCB, onConnectionClosedCB) {
|
51
|
+
return new Promise((r, e) => {
|
52
|
+
const urlParams = new URLSearchParams(params);
|
53
|
+
const url = `ws://${host}:${port}?${urlParams}`;
|
54
|
+
const ws = new websocket.WebSocket(url, [token]);
|
55
|
+
logger.info("created ws");
|
56
|
+
const client = new ChatClient(ws, onMessageCB, onConnectionClosedCB);
|
57
|
+
ws.onopen = async () => {
|
58
|
+
logger.info("opened");
|
59
|
+
ws.onmessage = (ev) => {
|
60
|
+
try {
|
61
|
+
const msgData = ev.data;
|
62
|
+
if (typeof msgData !== "string") {
|
63
|
+
throw `expected "string" data, got ${typeof msgData}`;
|
64
|
+
}
|
65
|
+
logger.debug(`[client.onmessage]: ${msgData}`);
|
66
|
+
const msg = JSON.parse(msgData);
|
67
|
+
client.onMessageCB(msg);
|
68
|
+
}
|
69
|
+
catch (e) {
|
70
|
+
client.close();
|
71
|
+
throw e;
|
72
|
+
}
|
73
|
+
};
|
74
|
+
r(client);
|
75
|
+
};
|
76
|
+
ws.onclose = (err) => {
|
77
|
+
logger.info("closed");
|
78
|
+
logger.info(`[client] WebSocket connection closed: ${JSON.stringify(err)}`);
|
79
|
+
client.closed = true;
|
80
|
+
onConnectionClosedCB();
|
81
|
+
};
|
82
|
+
ws.onerror = (error) => {
|
83
|
+
logger.error("[client] WebSocket error:", JSON.stringify(error));
|
84
|
+
e(error);
|
85
|
+
client.closed = true;
|
86
|
+
onConnectionClosedCB();
|
87
|
+
};
|
88
|
+
});
|
89
|
+
}
|
90
|
+
// public static async initWithSession(
|
91
|
+
// host: string,
|
92
|
+
// port: number,
|
93
|
+
// token: string,
|
94
|
+
// sessionId: string,
|
95
|
+
// onMessageCB: OnMessageCallback,
|
96
|
+
// onConnectionClosedCB: OnConnectionClosedCallback
|
97
|
+
// ): Promise<ChatClient> {
|
98
|
+
// return ChatClient.initWithParams(
|
99
|
+
// host,
|
100
|
+
// port,
|
101
|
+
// token,
|
102
|
+
// { session_id: sessionId },
|
103
|
+
// onMessageCB,
|
104
|
+
// onConnectionClosedCB
|
105
|
+
// );
|
106
|
+
// }
|
107
|
+
static async init(host, port, token, onMessageCB, onConnectionClosedCB, sessionId = "untitled", agentProfileId = undefined) {
|
108
|
+
const params = { session_id: sessionId };
|
109
|
+
if (agentProfileId) {
|
110
|
+
params["agent_profile_id"] = agentProfileId;
|
111
|
+
}
|
112
|
+
return ChatClient.initWithParams(host, port, token, params, onMessageCB, onConnectionClosedCB);
|
113
|
+
}
|
114
|
+
sendMessage(message) {
|
115
|
+
(0, assert_1.strict)(!this.closed);
|
116
|
+
const data = JSON.stringify(message);
|
117
|
+
this.ws.send(data);
|
118
|
+
}
|
119
|
+
close() {
|
120
|
+
this.closed = true;
|
121
|
+
this.onConnectionClosedCB();
|
122
|
+
this.ws.close();
|
123
|
+
}
|
124
|
+
}
|
125
|
+
exports.ChatClient = ChatClient;
|
126
|
+
// TODO: remove this
|
@@ -0,0 +1,173 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.ConversationManager = exports.OpenSession = exports.UserAlreadyConnected = void 0;
|
4
|
+
const uuid_1 = require("uuid");
|
5
|
+
const assert_1 = require("assert");
|
6
|
+
const sdk_1 = require("@xalia/xmcp/sdk");
|
7
|
+
const agentUtils_1 = require("../agent/agentUtils");
|
8
|
+
const asyncQueue_1 = require("./asyncQueue");
|
9
|
+
const asyncLock_1 = require("../utils/asyncLock");
|
10
|
+
const logger = (0, sdk_1.getLogger)();
|
11
|
+
class UserAlreadyConnected extends Error {
|
12
|
+
constructor() {
|
13
|
+
super("User already connected to the conversation");
|
14
|
+
}
|
15
|
+
}
|
16
|
+
exports.UserAlreadyConnected = UserAlreadyConnected;
|
17
|
+
/**
|
18
|
+
* Describes a Session (conversation) with connected participants.
|
19
|
+
*/
|
20
|
+
class OpenSession {
|
21
|
+
constructor(agent, sudoMcpServerManager, onEmpty) {
|
22
|
+
// public agent: Agent,
|
23
|
+
this.agent = agent;
|
24
|
+
this.skillManager = sudoMcpServerManager;
|
25
|
+
this.onEmpty = onEmpty;
|
26
|
+
this.connections = {};
|
27
|
+
this.messageQueue = new asyncQueue_1.AsyncQueue((m) => this.onMessage(m));
|
28
|
+
this.curAgentMsgId = undefined;
|
29
|
+
}
|
30
|
+
join(userName, ws) {
|
31
|
+
if (this.connections[userName]) {
|
32
|
+
throw new UserAlreadyConnected();
|
33
|
+
}
|
34
|
+
// Inform any other participants, and add the WebSocket to the map.
|
35
|
+
this.broadcast({ type: "user_joined", user: userName });
|
36
|
+
this.connections[userName] = ws;
|
37
|
+
ws.on("message", async (message) => {
|
38
|
+
logger.debug(`[convMgr]: got message: (from ${userName}): ${message}`);
|
39
|
+
const msgStr = message.toString();
|
40
|
+
const msg = JSON.parse(msgStr);
|
41
|
+
await this.messageQueue.enqueueAsync({ msg, from: userName });
|
42
|
+
});
|
43
|
+
ws.on("close", () => {
|
44
|
+
logger.debug(`[convMgr]: ${userName} closed`);
|
45
|
+
// Remove our connection then inform any other participants
|
46
|
+
delete this.connections[userName];
|
47
|
+
this.broadcast({ type: "user_left", user: userName });
|
48
|
+
if (Object.keys(this.connections).length == 0) {
|
49
|
+
this.onEmpty();
|
50
|
+
}
|
51
|
+
});
|
52
|
+
}
|
53
|
+
/**
|
54
|
+
* Called once for each message. Messages are queued to avoid overlapping
|
55
|
+
* calls to the LLM.
|
56
|
+
*/
|
57
|
+
async onMessage(queuedMessage) {
|
58
|
+
logger.debug(`[onMessage]: processing (${queuedMessage.from}) ` +
|
59
|
+
`${JSON.stringify(queuedMessage.msg)}`);
|
60
|
+
const msg = queuedMessage.msg;
|
61
|
+
switch (msg.type) {
|
62
|
+
case "msg":
|
63
|
+
return this.onChatMessage(queuedMessage);
|
64
|
+
default:
|
65
|
+
throw `unknown message: ${JSON.stringify(queuedMessage)}`;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
async onAgentMessage(msg, end) {
|
69
|
+
logger.debug(`[onAgentMessage] msg: ${msg}, end: ${end}`);
|
70
|
+
(0, assert_1.strict)(this.curAgentMsgId);
|
71
|
+
this.broadcast({
|
72
|
+
type: "agent_msg_chunk",
|
73
|
+
message_id: this.curAgentMsgId,
|
74
|
+
message: msg,
|
75
|
+
end,
|
76
|
+
});
|
77
|
+
}
|
78
|
+
async onChatMessage(queuedMessage) {
|
79
|
+
const msg = queuedMessage.msg;
|
80
|
+
const userToken = queuedMessage.from;
|
81
|
+
const msgId = (0, uuid_1.v4)();
|
82
|
+
this.broadcast({
|
83
|
+
type: "user_msg",
|
84
|
+
message_id: msgId,
|
85
|
+
message: msg.message,
|
86
|
+
from: userToken,
|
87
|
+
});
|
88
|
+
// Messages will be handled by the Agent.onMessage callback. We await the
|
89
|
+
// response here before processing further messages.
|
90
|
+
this.curAgentMsgId = `${msgId}-resp`;
|
91
|
+
await this.agent.userMessage(msg.message, undefined, userToken);
|
92
|
+
}
|
93
|
+
broadcast(msg) {
|
94
|
+
logger.info(`[broadcast]: broadcast msg: ${JSON.stringify(msg)}`);
|
95
|
+
const msgString = JSON.stringify(msg);
|
96
|
+
for (const ws of Object.values(this.connections)) {
|
97
|
+
ws.send(msgString);
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
exports.OpenSession = OpenSession;
|
102
|
+
/**
|
103
|
+
* Handles forwarding of messages between all participants of a session, as
|
104
|
+
* well as messages to/from and Agent.
|
105
|
+
*/
|
106
|
+
class ConversationManager {
|
107
|
+
constructor(db, llmUrl, xmcpUrl) {
|
108
|
+
this.db = db;
|
109
|
+
this.llmUrl = llmUrl;
|
110
|
+
this.xmcpUrl = xmcpUrl;
|
111
|
+
this.openSessionsLock = new asyncLock_1.AsyncLock();
|
112
|
+
this.openSessions = {};
|
113
|
+
}
|
114
|
+
async join(sessionId, llmApiKey, xmcpApiKey, userData, ws) {
|
115
|
+
await this.openSessionsLock.lockAndProcess(() => {
|
116
|
+
return this.getOrCreateAndSubscribe(sessionId, llmApiKey, xmcpApiKey, userData.nickname || userData.user_uuid, ws);
|
117
|
+
});
|
118
|
+
}
|
119
|
+
/**
|
120
|
+
* Must be called while holding the openSessionsLock
|
121
|
+
*/
|
122
|
+
async getOrCreateAndSubscribe(sessionId, llmApiKey, xmcpApiKey, userName, ws) {
|
123
|
+
let openSession = this.openSessions[sessionId];
|
124
|
+
if (openSession) {
|
125
|
+
openSession.join(userName, ws);
|
126
|
+
return openSession;
|
127
|
+
}
|
128
|
+
// Create a new session
|
129
|
+
// TODO: The owner of llmApiKey and xmcpApiKey may not be the owner of the
|
130
|
+
// session. Should we create the Agent and SudoMcpServerManager with the
|
131
|
+
// session-owners api key?
|
132
|
+
const onEmpty = () => {
|
133
|
+
logger.debug(`session ${sessionId} empty. removing`);
|
134
|
+
delete this.openSessions[sessionId];
|
135
|
+
};
|
136
|
+
const sessionData = await this.db.getSessionById(sessionId);
|
137
|
+
if (!sessionData) {
|
138
|
+
throw `no such session ${sessionId}`;
|
139
|
+
}
|
140
|
+
const conversation = sessionData.conversation;
|
141
|
+
const agentProfile = await this.db.getAgentProfileById(sessionData.agent_profile_uuid);
|
142
|
+
if (!agentProfile) {
|
143
|
+
throw `no such agent profile ${sessionData.agent_profile_uuid}`;
|
144
|
+
}
|
145
|
+
const platform = {
|
146
|
+
openUrl: (_url, _authResultP, _displayName) => {
|
147
|
+
throw "unimpl";
|
148
|
+
},
|
149
|
+
load: (_filename) => {
|
150
|
+
throw "unimpl";
|
151
|
+
},
|
152
|
+
};
|
153
|
+
// Forward agent messages to the OpenSession (once it is iniailized
|
154
|
+
const context = {};
|
155
|
+
const onMessage = async (msg, end) => {
|
156
|
+
logger.debug(`[onMessage] msg: ${msg}, end: ${end}`);
|
157
|
+
(0, assert_1.strict)(context.openSession);
|
158
|
+
context.openSession.onAgentMessage(msg, end);
|
159
|
+
};
|
160
|
+
const onToolCall = async (toolCall) => {
|
161
|
+
logger.debug(`[onToolCall] : ${JSON.stringify(toolCall)}`);
|
162
|
+
return true;
|
163
|
+
};
|
164
|
+
const xmcpConfig = sdk_1.Configuration.new(xmcpApiKey, this.xmcpUrl, false);
|
165
|
+
const [agent, smsm] = await (0, agentUtils_1.createAgentWithSkills)(this.llmUrl, agentProfile, onMessage, onToolCall, platform, llmApiKey, xmcpConfig, undefined, conversation, true);
|
166
|
+
openSession = new OpenSession(agent, smsm, onEmpty);
|
167
|
+
context.openSession = openSession;
|
168
|
+
this.openSessions[sessionId] = openSession;
|
169
|
+
openSession.join(userName, ws);
|
170
|
+
return openSession;
|
171
|
+
}
|
172
|
+
}
|
173
|
+
exports.ConversationManager = ConversationManager;
|