@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.
Files changed (76) hide show
  1. package/README.md +46 -7
  2. package/dist/{agent.js → agent/src/agent/agent.js} +5 -4
  3. package/dist/{agentUtils.js → agent/src/agent/agentUtils.js} +10 -9
  4. package/dist/{mcpServerManager.js → agent/src/agent/mcpServerManager.js} +2 -1
  5. package/dist/{sudoMcpServerManager.js → agent/src/agent/sudoMcpServerManager.js} +4 -4
  6. package/dist/agent/src/chat/apiKeyManager.js +23 -0
  7. package/dist/agent/src/chat/asyncQueue.js +41 -0
  8. package/dist/agent/src/chat/client.js +126 -0
  9. package/dist/agent/src/chat/conversationManager.js +173 -0
  10. package/dist/agent/src/chat/db.js +186 -0
  11. package/dist/agent/src/chat/messages.js +2 -0
  12. package/dist/agent/src/chat/server.js +158 -0
  13. package/dist/agent/src/index.js +2 -0
  14. package/dist/agent/src/test/db.test.js +73 -0
  15. package/dist/{test → agent/src/test}/imageLoad.test.js +1 -1
  16. package/dist/{test → agent/src/test}/mcpServerManager.test.js +1 -1
  17. package/dist/{test → agent/src/test}/prompt.test.js +1 -1
  18. package/dist/{test → agent/src/test}/sudoMcpServerManager.test.js +3 -3
  19. package/dist/{chat.js → agent/src/tool/agentChat.js} +5 -5
  20. package/dist/{main.js → agent/src/tool/agentMain.js} +9 -15
  21. package/dist/agent/src/tool/chatMain.js +207 -0
  22. package/dist/agent/src/tool/main.js +54 -0
  23. package/dist/{options.js → agent/src/tool/options.js} +36 -2
  24. package/dist/agent/src/utils/asyncLock.js +45 -0
  25. package/dist/supabase/database.types.js +8 -0
  26. package/eslint.config.mjs +14 -14
  27. package/package.json +9 -15
  28. package/scripts/test_chat +84 -0
  29. package/src/{agent.ts → agent/agent.ts} +22 -11
  30. package/src/{agentUtils.ts → agent/agentUtils.ts} +13 -14
  31. package/src/{mcpServerManager.ts → agent/mcpServerManager.ts} +2 -1
  32. package/src/{sudoMcpServerManager.ts → agent/sudoMcpServerManager.ts} +3 -3
  33. package/src/chat/apiKeyManager.ts +24 -0
  34. package/src/chat/asyncQueue.ts +51 -0
  35. package/src/chat/client.ts +142 -0
  36. package/src/chat/conversationManager.ts +283 -0
  37. package/src/chat/db.ts +264 -0
  38. package/src/chat/messages.ts +91 -0
  39. package/src/chat/server.ts +177 -0
  40. package/src/test/db.test.ts +103 -0
  41. package/src/test/imageLoad.test.ts +1 -1
  42. package/src/test/mcpServerManager.test.ts +1 -1
  43. package/src/test/prompt.test.ts +1 -1
  44. package/src/test/sudoMcpServerManager.test.ts +6 -10
  45. package/src/{chat.ts → tool/agentChat.ts} +26 -24
  46. package/src/{main.ts → tool/agentMain.ts} +12 -19
  47. package/src/tool/chatMain.ts +250 -0
  48. package/src/{files.ts → tool/files.ts} +1 -1
  49. package/src/tool/main.ts +25 -0
  50. package/src/{nodePlatform.ts → tool/nodePlatform.ts} +1 -1
  51. package/src/{options.ts → tool/options.ts} +40 -1
  52. package/src/utils/asyncLock.ts +43 -0
  53. package/test_data/simplecalc_profile.json +1 -1
  54. package/test_data/sudomcp_import_profile.json +1 -1
  55. package/test_data/test_script_profile.json +1 -1
  56. package/tsconfig.json +1 -1
  57. package/scripts/test_script +0 -60
  58. /package/dist/{dummyLLM.js → agent/src/agent/dummyLLM.js} +0 -0
  59. /package/dist/{iplatform.js → agent/src/agent/iplatform.js} +0 -0
  60. /package/dist/{llm.js → agent/src/agent/llm.js} +0 -0
  61. /package/dist/{openAILLM.js → agent/src/agent/openAILLM.js} +0 -0
  62. /package/dist/{openAILLMStreaming.js → agent/src/agent/openAILLMStreaming.js} +0 -0
  63. /package/dist/{tokenAuth.js → agent/src/agent/tokenAuth.js} +0 -0
  64. /package/dist/{tools.js → agent/src/agent/tools.js} +0 -0
  65. /package/dist/{files.js → agent/src/tool/files.js} +0 -0
  66. /package/dist/{nodePlatform.js → agent/src/tool/nodePlatform.js} +0 -0
  67. /package/dist/{prompt.js → agent/src/tool/prompt.js} +0 -0
  68. /package/src/{dummyLLM.ts → agent/dummyLLM.ts} +0 -0
  69. /package/src/{iplatform.ts → agent/iplatform.ts} +0 -0
  70. /package/src/{llm.ts → agent/llm.ts} +0 -0
  71. /package/src/{openAILLM.ts → agent/openAILLM.ts} +0 -0
  72. /package/src/{openAILLMStreaming.ts → agent/openAILLMStreaming.ts} +0 -0
  73. /package/src/{tokenAuth.ts → agent/tokenAuth.ts} +0 -0
  74. /package/src/{tools.ts → agent/tools.ts} +0 -0
  75. /package/src/{test/prompt.test.src → index.ts} +0 -0
  76. /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
- - Agent (for conversation, ChatCompletion)
44
- - McpServerManager (to enable, disable tools that have been added)
45
- - SudoMcpServerManager (to access catalog of SudoMCP servers, add to McpServerManager)
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
- - track list of available mcp servers (via sdk/ApiClient)
49
- - get the list of tools as required by UI (via sdk/ApiClient)
50
- - add tools to McpServerManager
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.createAgentAndSudoMcpServerManager = createAgentAndSudoMcpServerManager;
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 createAgentAndSudoMcpServerManager(url, agentProfile, onMessage, onToolCall, platform, openaiApiKey, sudomcpConfig, authorizedUrl, conversation, stream = false) {
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(url, agentProfile.model, agentProfile.system_prompt, onMessage, onToolCall, platform, openaiApiKey, stream);
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("[createAgentAndSudoMcpServerManager] creating SudoMcpServerManager.");
61
- const sudoMcpServerManager = await sudoMcpServerManager_1.SudoMcpServerManager.initialize(agent.getMcpServerManager(), platform.openUrl, sudomcpConfig.backend_url, sudomcpConfig.api_key, authorizedUrl);
62
- logger.debug("[createAgentAndSudoMcpServerManager] restore mcp settings:" +
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("[createAgentAndSudoMcpServerManager] done");
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 createAgentAndSudoMcpServerManager(url, agentProfile, onMessage, onToolCall, platform, openaiApiKey, sudomcpConfig, undefined, conversation);
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
- // TODO: is this catch necessary?
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.SudoMcpServerManager = exports.LOCAL_SERVER_URL = void 0;
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 SudoMcpServerManager {
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 SudoMcpServerManager(mcpServerManager, apiClient, mcpServers, mcpServersMap, {}, openUrl, authorized_url);
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.SudoMcpServerManager = SudoMcpServerManager;
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;