@xalia/agent 1.0.19

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 (64) hide show
  1. package/.prettierrc.json +11 -0
  2. package/README.md +57 -0
  3. package/dist/agent.js +278 -0
  4. package/dist/agentUtils.js +88 -0
  5. package/dist/chat.js +278 -0
  6. package/dist/dummyLLM.js +28 -0
  7. package/dist/files.js +115 -0
  8. package/dist/iplatform.js +2 -0
  9. package/dist/llm.js +2 -0
  10. package/dist/main.js +136 -0
  11. package/dist/mcpServerManager.js +269 -0
  12. package/dist/nodePlatform.js +61 -0
  13. package/dist/openAILLM.js +31 -0
  14. package/dist/options.js +79 -0
  15. package/dist/prompt.js +83 -0
  16. package/dist/sudoMcpServerManager.js +174 -0
  17. package/dist/test/imageLoad.test.js +14 -0
  18. package/dist/test/mcpServerManager.test.js +71 -0
  19. package/dist/test/prompt.test.js +26 -0
  20. package/dist/test/sudoMcpServerManager.test.js +49 -0
  21. package/dist/tokenAuth.js +39 -0
  22. package/dist/tools.js +44 -0
  23. package/eslint.config.mjs +25 -0
  24. package/frog.png +0 -0
  25. package/package.json +41 -0
  26. package/scripts/git_message +31 -0
  27. package/scripts/git_wip +21 -0
  28. package/scripts/pr_message +18 -0
  29. package/scripts/pr_review +16 -0
  30. package/scripts/sudomcp_import +23 -0
  31. package/scripts/test_script +60 -0
  32. package/src/agent.ts +357 -0
  33. package/src/agentUtils.ts +188 -0
  34. package/src/chat.ts +325 -0
  35. package/src/dummyLLM.ts +36 -0
  36. package/src/files.ts +95 -0
  37. package/src/iplatform.ts +11 -0
  38. package/src/llm.ts +12 -0
  39. package/src/main.ts +171 -0
  40. package/src/mcpServerManager.ts +365 -0
  41. package/src/nodePlatform.ts +24 -0
  42. package/src/openAILLM.ts +43 -0
  43. package/src/options.ts +103 -0
  44. package/src/prompt.ts +93 -0
  45. package/src/sudoMcpServerManager.ts +268 -0
  46. package/src/test/imageLoad.test.ts +14 -0
  47. package/src/test/mcpServerManager.test.ts +98 -0
  48. package/src/test/prompt.test.src +0 -0
  49. package/src/test/prompt.test.ts +26 -0
  50. package/src/test/sudoMcpServerManager.test.ts +63 -0
  51. package/src/tokenAuth.ts +50 -0
  52. package/src/tools.ts +57 -0
  53. package/test_data/background_test_profile.json +7 -0
  54. package/test_data/background_test_script.json +11 -0
  55. package/test_data/dummyllm_script_simplecalc.json +28 -0
  56. package/test_data/git_message_profile.json +4 -0
  57. package/test_data/git_wip_system.txt +5 -0
  58. package/test_data/pr_message_profile.json +4 -0
  59. package/test_data/pr_review_profile.json +4 -0
  60. package/test_data/prompt_simplecalc.txt +1 -0
  61. package/test_data/simplecalc_profile.json +4 -0
  62. package/test_data/sudomcp_import_profile.json +4 -0
  63. package/test_data/test_script_profile.json +9 -0
  64. package/tsconfig.json +13 -0
package/src/agent.ts ADDED
@@ -0,0 +1,357 @@
1
+ import * as dotenv from "dotenv";
2
+ import { OpenAI } from "openai";
3
+ import { McpServerManager, McpServerManagerSettings } from "./mcpServerManager";
4
+ import {
5
+ ChatCompletionContentPart,
6
+ ChatCompletionUserMessageParam,
7
+ } from "openai/resources.mjs";
8
+ import { strict as assert } from "assert";
9
+ import { OpenAILLM } from "./openAILLM";
10
+ import { ILLM } from "./llm";
11
+ import { DummyLLM } from "./dummyLLM";
12
+ import { getLogger } from "@xalia/xmcp/sdk";
13
+
14
+ export type ToolHandler = (args: unknown) => string;
15
+
16
+ export type McpServerUrls = (name: string) => string;
17
+
18
+ // Role: If content, give it to UI
19
+ export type OnMessageCB = {
20
+ (msg: OpenAI.ChatCompletionMessageParam, msgEnd: boolean): Promise<void>;
21
+ };
22
+
23
+ // Role: If tool calls, prompt for permission to handle them
24
+ export type OnToolCallCB = {
25
+ (msg: OpenAI.ChatCompletionMessageToolCall): Promise<boolean>;
26
+ };
27
+
28
+ dotenv.config();
29
+ const logger = getLogger();
30
+
31
+ export class AgentProfile {
32
+ constructor(
33
+ /// The llm provider endpoint, or dummy llm filename. `undefined` means
34
+ /// openai.
35
+ public llm_url: string | undefined,
36
+
37
+ /// "dummy" means use the dummy LLM, in which case llmUrl refers to the
38
+ /// filename. `undefined` means default for the provider.
39
+ public model: string | undefined,
40
+
41
+ /// System prompt
42
+ public system_prompt: string,
43
+
44
+ /// MCP server settings.
45
+ public mcp_settings: McpServerManagerSettings
46
+ ) {}
47
+
48
+ public static fromJSONObj(obj: Record<string, unknown>): AgentProfile {
49
+ assert(typeof obj === "object");
50
+ assert(
51
+ typeof obj.llm_url === "string" || typeof obj.llm_url === "undefined"
52
+ );
53
+ assert(typeof obj.model === "string" || typeof obj.model === "undefined");
54
+ assert(typeof obj.system_prompt === "string");
55
+ assert(typeof obj.mcp_settings === "object");
56
+ return new AgentProfile(
57
+ obj.llm_url,
58
+ obj.model,
59
+ obj.system_prompt,
60
+ obj.mcp_settings as McpServerManagerSettings
61
+ );
62
+ }
63
+ }
64
+
65
+ export class Agent {
66
+ private toolHandlers: { [toolName: string]: ToolHandler } = {};
67
+
68
+ private constructor(
69
+ public onMessage: OnMessageCB,
70
+ public onToolCall: OnToolCallCB,
71
+ private messages: OpenAI.ChatCompletionMessageParam[],
72
+ private mcpServerManager: McpServerManager,
73
+ private tools: OpenAI.ChatCompletionTool[],
74
+ private llm: ILLM
75
+ ) {}
76
+
77
+ public static async initializeWithLLM(
78
+ onMessage: OnMessageCB,
79
+ onToolCall: OnToolCallCB,
80
+ systemPrompt: string | undefined,
81
+ llm: ILLM
82
+ ): Promise<Agent> {
83
+ // Initialize messages with system prompt
84
+ const messages = [
85
+ {
86
+ role: "system",
87
+ content: systemPrompt ?? "You are a helpful assistant",
88
+ } as OpenAI.ChatCompletionMessageParam,
89
+ ];
90
+
91
+ // Create the server manager
92
+ const mcpServerManager = new McpServerManager();
93
+
94
+ return new Agent(
95
+ onMessage,
96
+ onToolCall,
97
+ messages,
98
+ mcpServerManager,
99
+ [],
100
+ llm
101
+ );
102
+ }
103
+
104
+ public static async initialize(
105
+ onMessage: OnMessageCB,
106
+ onToolCall: OnToolCallCB,
107
+ systemPrompt: string,
108
+ openaiApiUrl: string | undefined,
109
+ openaiApiKey: string,
110
+ model: string | undefined
111
+ ): Promise<Agent> {
112
+ return Agent.initializeWithLLM(
113
+ onMessage,
114
+ onToolCall,
115
+ systemPrompt,
116
+ new OpenAILLM(openaiApiKey, openaiApiUrl, model)
117
+ );
118
+ }
119
+
120
+ public static async initializeDummy(
121
+ onMessage: OnMessageCB,
122
+ onToolCall: OnToolCallCB,
123
+ systemPrompt: string,
124
+ responses: OpenAI.Chat.Completions.ChatCompletion.Choice[]
125
+ ): Promise<Agent> {
126
+ return Agent.initializeWithLLM(
127
+ onMessage,
128
+ onToolCall,
129
+ systemPrompt,
130
+ new DummyLLM(responses)
131
+ );
132
+ }
133
+
134
+ public async shutdown(): Promise<void> {
135
+ return this.mcpServerManager.shutdown();
136
+ }
137
+
138
+ public getAgentProfile(): AgentProfile {
139
+ return new AgentProfile(
140
+ this.llm.getUrl(),
141
+ this.llm.getModel(),
142
+ this.getSystemMessage(),
143
+ this.mcpServerManager.getMcpServerSettings()
144
+ );
145
+ }
146
+
147
+ public getConversation(): OpenAI.ChatCompletionMessageParam[] {
148
+ assert(
149
+ this.messages[0].role == "system",
150
+ "first message must have system role"
151
+ );
152
+ // Return a copy so future modifications to `this.messages` don't impact
153
+ // the callers copy.
154
+ return structuredClone(this.messages.slice(1));
155
+ }
156
+
157
+ public setConversation(messages: OpenAI.ChatCompletionMessageParam[]) {
158
+ assert(this.messages[0].role == "system");
159
+ assert(messages[0].role != "system", "conversation contains system msg");
160
+
161
+ const newMessages: OpenAI.ChatCompletionMessageParam[] = [this.messages[0]];
162
+ this.messages = newMessages.concat(structuredClone(messages));
163
+ }
164
+
165
+ public getMcpServerManager(): McpServerManager {
166
+ return this.mcpServerManager;
167
+ }
168
+
169
+ public async userMessage(
170
+ msg?: string,
171
+ imageB64?: string
172
+ ): Promise<OpenAI.ChatCompletionMessageParam | undefined> {
173
+ const userMessage = createUserMessage(msg, imageB64);
174
+ if (!userMessage) {
175
+ return undefined;
176
+ }
177
+
178
+ this.messages.push(userMessage);
179
+ let completion = await this.chatCompletion();
180
+
181
+ let message = completion.choices[0].message;
182
+ this.messages.push(message);
183
+
184
+ if (message.content) {
185
+ await this.onMessage(message, true);
186
+ }
187
+
188
+ // While there are tool calls to make, make them and loop
189
+
190
+ while (message.tool_calls && message.tool_calls.length > 0) {
191
+ for (const toolCall of message.tool_calls ?? []) {
192
+ const approval = await this.onToolCall(toolCall);
193
+ if (approval) {
194
+ try {
195
+ const result = await this.doToolCall(toolCall);
196
+ logger.debug(`tool call result ${JSON.stringify(result)}`);
197
+ this.messages.push(result);
198
+ } catch (e) {
199
+ logger.error(`tool call error: ${e}`);
200
+ this.messages.push({
201
+ role: "tool",
202
+ tool_call_id: toolCall.id,
203
+ content: "Tool call failed.",
204
+ });
205
+ }
206
+ } else {
207
+ this.messages.push({
208
+ role: "tool",
209
+ tool_call_id: toolCall.id,
210
+ content: "User denied tool use request.",
211
+ });
212
+ }
213
+ }
214
+
215
+ completion = await this.chatCompletion();
216
+ message = completion.choices[0].message;
217
+ this.messages.push(message);
218
+
219
+ if (message.content) {
220
+ await this.onMessage(message, true);
221
+ }
222
+ }
223
+
224
+ return completion.choices[0].message;
225
+ }
226
+
227
+ public chooseModel(model: string) {
228
+ logger.debug(`Set model ${model}`);
229
+ assert(this.llm instanceof OpenAILLM);
230
+ this.llm.setModel(model);
231
+ }
232
+
233
+ /**
234
+ * Clear the conversation.
235
+ */
236
+ public resetConversation() {
237
+ assert(this.messages.length > 0);
238
+ // Keep only the system message
239
+ this.messages.splice(1);
240
+ }
241
+
242
+ public getSystemMessage(): string {
243
+ assert(this.messages[0].role === "system");
244
+ return this.messages[0].content as string;
245
+ }
246
+
247
+ /**
248
+ * Set the system prompt
249
+ */
250
+ public setSystemMessage(systemMsg: string) {
251
+ assert(this.messages[0].role === "system");
252
+ this.messages[0].content = systemMsg;
253
+ }
254
+
255
+ async chatCompletion(): Promise<OpenAI.Chat.Completions.ChatCompletion> {
256
+ let tools: OpenAI.ChatCompletionTool[] | undefined;
257
+ const enabledTools = this.tools.concat(
258
+ this.mcpServerManager.getOpenAITools()
259
+ );
260
+ if (enabledTools.length > 0) {
261
+ tools = enabledTools;
262
+ }
263
+ // logger.debug(
264
+ // `chatCompletion: tools: ${JSON.stringify(tools, undefined, 2)}`
265
+ // );
266
+ const completion = await this.llm.getConversationResponse(
267
+ this.messages,
268
+ tools
269
+ );
270
+ logger.debug(`Received chat completion ${JSON.stringify(completion)}`);
271
+ return completion;
272
+ }
273
+
274
+ public toolNames(): string[] {
275
+ return this.mcpServerManager
276
+ .getOpenAITools()
277
+ .map((tool) => tool.function.name);
278
+ }
279
+
280
+ public addTool(tool: OpenAI.ChatCompletionTool, handler: ToolHandler) {
281
+ const name = tool.function.name;
282
+ if (this.toolHandlers[name]) {
283
+ throw `tool ${name} already added`;
284
+ }
285
+
286
+ logger.debug(`Adding tool ${name}`);
287
+
288
+ this.tools.push(tool);
289
+ this.toolHandlers[name] = handler;
290
+ }
291
+
292
+ async doToolCall(
293
+ toolCall: OpenAI.ChatCompletionMessageToolCall
294
+ ): Promise<OpenAI.ChatCompletionToolMessageParam> {
295
+ const name = toolCall.function.name;
296
+ const args = JSON.parse(toolCall.function.arguments);
297
+
298
+ let result: string | undefined = undefined;
299
+ const handler = this.toolHandlers[name];
300
+ if (handler) {
301
+ logger.debug(` found agent tool ${name} ...`);
302
+ result = handler(args);
303
+ } else {
304
+ result = await this.mcpServerManager.invoke(name, args);
305
+ }
306
+ return {
307
+ role: "tool",
308
+ tool_call_id: toolCall.id,
309
+ content: result.toString(),
310
+ };
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Returns the ChatCompletionMessageParam constructed from (optional) text and
316
+ * (optional) image. If neither is given (null message), then undefined is
317
+ * returned.
318
+ **/
319
+ export function createUserMessage(
320
+ msg?: string,
321
+ imageB64?: string
322
+ ): ChatCompletionUserMessageParam | undefined {
323
+ const content = (() => {
324
+ if (!imageB64) {
325
+ if (!msg) {
326
+ return undefined;
327
+ }
328
+ return msg;
329
+ }
330
+
331
+ const content: ChatCompletionContentPart[] = [];
332
+ if (msg) {
333
+ content.push({
334
+ type: "text",
335
+ text: msg,
336
+ });
337
+ }
338
+ if (imageB64) {
339
+ content.push({
340
+ type: "image_url",
341
+ image_url: {
342
+ url: imageB64,
343
+ },
344
+ });
345
+ }
346
+ return content;
347
+ })();
348
+
349
+ if (!content) {
350
+ return undefined;
351
+ }
352
+
353
+ return {
354
+ role: "user",
355
+ content,
356
+ };
357
+ }
@@ -0,0 +1,188 @@
1
+ import { getLogger } from "@xalia/xmcp/sdk";
2
+ import { Agent, AgentProfile, OnMessageCB, OnToolCallCB } from "./agent";
3
+ import { IPlatform } from "./iplatform";
4
+ import { SudoMcpServerManager } from "./sudoMcpServerManager";
5
+ import OpenAI from "openai";
6
+ import { Configuration as SudoMcpConfiguration } from "@xalia/xmcp/sdk";
7
+
8
+ const logger = getLogger();
9
+
10
+ /**
11
+ * Util function to create an Agent from some config information.
12
+ */
13
+ async function createAgent(
14
+ llmUrl: string | undefined,
15
+ model: string | undefined,
16
+ systemPrompt: string,
17
+ onMessage: OnMessageCB,
18
+ onToolCall: OnToolCallCB,
19
+ platform: IPlatform,
20
+ openaiApiKey: string | undefined
21
+ ): Promise<Agent> {
22
+ if (model === "dummy") {
23
+ if (!llmUrl) {
24
+ throw "AgentProfile.llmUrl must be set for dummy LLM";
25
+ }
26
+ logger.debug(`dummy model with script: ${llmUrl}`);
27
+ const script = await platform.load(llmUrl);
28
+ logger.debug(` script: ${script}`);
29
+ const responses: OpenAI.ChatCompletion.Choice[] = JSON.parse(script);
30
+ logger.debug(`Initializing Dummy Agent: ${llmUrl}`);
31
+ return Agent.initializeDummy(
32
+ onMessage,
33
+ onToolCall,
34
+ systemPrompt,
35
+ responses
36
+ );
37
+ }
38
+
39
+ if (!openaiApiKey) {
40
+ throw "Missing OpenAI API Key";
41
+ }
42
+
43
+ logger.debug(`Initializing Agent: ${llmUrl} - ${model}`);
44
+
45
+ return Agent.initialize(
46
+ onMessage,
47
+ onToolCall,
48
+ systemPrompt,
49
+ llmUrl,
50
+ openaiApiKey,
51
+ model
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Util function to create and initialize an Agent given an AgentProfile.
57
+ */
58
+ export async function createAgentAndSudoMcpServerManager(
59
+ agentProfile: AgentProfile,
60
+ onMessage: OnMessageCB,
61
+ onToolCall: OnToolCallCB,
62
+ platform: IPlatform,
63
+ openaiApiKey: string | undefined,
64
+ sudomcpConfig: SudoMcpConfiguration,
65
+ authorizedUrl: string | undefined,
66
+ conversation: OpenAI.ChatCompletionMessageParam[] | undefined
67
+ ): Promise<[Agent, SudoMcpServerManager]> {
68
+ // Create agent
69
+ logger.debug("[createAgentAndSudoMcpServerManager] creating agent ...");
70
+ const agent = await createAgent(
71
+ agentProfile.llm_url,
72
+ agentProfile.model,
73
+ agentProfile.system_prompt,
74
+ onMessage,
75
+ onToolCall,
76
+ platform,
77
+ openaiApiKey
78
+ );
79
+ if (conversation) {
80
+ agent.setConversation(conversation);
81
+ }
82
+
83
+ // Init SudoMcpServerManager
84
+ logger.debug(
85
+ "[createAgentAndSudoMcpServerManager] creating SudoMcpServerManager."
86
+ );
87
+ const sudoMcpServerManager = await SudoMcpServerManager.initialize(
88
+ agent.getMcpServerManager(),
89
+ platform.openUrl,
90
+ sudomcpConfig.backend_url,
91
+ sudomcpConfig.api_key,
92
+ authorizedUrl
93
+ );
94
+ logger.debug(
95
+ "[createAgentAndSudoMcpServerManager] restore mcp settings:" +
96
+ JSON.stringify(agentProfile.mcp_settings)
97
+ );
98
+ await sudoMcpServerManager.restoreMcpSettings(
99
+ agentProfile.mcp_settings,
100
+ sudomcpConfig.server_configs
101
+ );
102
+
103
+ logger.debug("[createAgentAndSudoMcpServerManager] done");
104
+ return [agent, sudoMcpServerManager];
105
+ }
106
+
107
+ /**
108
+ * An "non-interactive" agent is one which is not intended to be used
109
+ * interactively (settings cannot be dyanmically adjusted, intermediate
110
+ * messages are not used by the caller, the user does not need to approve tool
111
+ * calls, etc).
112
+ */
113
+ export async function createNonInteractiveAgent(
114
+ agentProfile: AgentProfile,
115
+ conversation: OpenAI.ChatCompletionMessageParam[] | undefined,
116
+ platform: IPlatform,
117
+ openaiApiKey: string | undefined,
118
+ sudomcpConfig: SudoMcpConfiguration,
119
+ approveToolsUpTo: number
120
+ ): Promise<Agent> {
121
+ let remainingToolCalls = approveToolsUpTo;
122
+ const onMessage = async () => {};
123
+ const onToolCall = async () => {
124
+ if (remainingToolCalls !== 0) {
125
+ --remainingToolCalls;
126
+ return true;
127
+ }
128
+ return false;
129
+ };
130
+
131
+ const [agent, _] = await createAgentAndSudoMcpServerManager(
132
+ agentProfile,
133
+ onMessage,
134
+ onToolCall,
135
+ platform,
136
+ openaiApiKey,
137
+ sudomcpConfig,
138
+ undefined,
139
+ conversation
140
+ );
141
+
142
+ return agent;
143
+ }
144
+
145
+ /**
146
+ * Create an Agent (from the AgentProfile), pass it a single prompt and output
147
+ * the response.
148
+ */
149
+ export async function runOneShot(
150
+ agentProfile: AgentProfile,
151
+ conversation: OpenAI.ChatCompletionMessageParam[] | undefined,
152
+ platform: IPlatform,
153
+ prompt: string,
154
+ image: string | undefined,
155
+ openaiApiKey: string | undefined,
156
+ sudomcpConfig: SudoMcpConfiguration,
157
+ approveToolsUpTo: number
158
+ ): Promise<{
159
+ response: string;
160
+ conversation: OpenAI.ChatCompletionMessageParam[];
161
+ }> {
162
+ logger.debug("[runOneShot]: start");
163
+
164
+ // Create a non-interactive agent and pass any prompt/ image to it. Return
165
+ // the first answer.
166
+
167
+ const agent = await createNonInteractiveAgent(
168
+ agentProfile,
169
+ conversation,
170
+ platform,
171
+ openaiApiKey,
172
+ sudomcpConfig,
173
+ approveToolsUpTo
174
+ );
175
+
176
+ const response = await agent.userMessage(prompt, image);
177
+ await agent.shutdown();
178
+ logger.debug("[runOneShot]: shutdown done");
179
+
180
+ if (!response) {
181
+ throw "No message returned from agent";
182
+ }
183
+
184
+ return {
185
+ response: "" + response.content,
186
+ conversation: agent.getConversation(),
187
+ };
188
+ }