@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/src/chat/db.ts
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
2
|
+
import type * as supabase from "../../../supabase/database.types";
|
3
|
+
import {
|
4
|
+
AgentProfile,
|
5
|
+
ApiKey,
|
6
|
+
getLogger,
|
7
|
+
SavedAgentProfile,
|
8
|
+
} from "@xalia/xmcp/sdk";
|
9
|
+
|
10
|
+
const logger = getLogger();
|
11
|
+
|
12
|
+
export const SUPABASE_LOCAL_URL = "http://127.0.0.1:54321";
|
13
|
+
export const SUPABASE_LOCAL_KEY =
|
14
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiw" +
|
15
|
+
"icm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJz" +
|
16
|
+
"dJsyH-qQwv8Hdp7fsn3W0YpN81IU";
|
17
|
+
|
18
|
+
/**
|
19
|
+
* 'name' -> 'name'
|
20
|
+
* 'space/name' -> ['space', 'name']
|
21
|
+
*/
|
22
|
+
export function resolveCompoundName(name: string): string | [string, string] {
|
23
|
+
const components = name.split("/");
|
24
|
+
if (components.length === 1) {
|
25
|
+
return name;
|
26
|
+
}
|
27
|
+
if (components.length !== 2) {
|
28
|
+
throw "invalid compound name";
|
29
|
+
}
|
30
|
+
return components as [string, string];
|
31
|
+
}
|
32
|
+
|
33
|
+
export type UserData = {
|
34
|
+
user_uuid: supabase.Tables<"api_keys">["user_uuid"];
|
35
|
+
nickname: supabase.Tables<"users">["nickname"];
|
36
|
+
};
|
37
|
+
|
38
|
+
export class Database {
|
39
|
+
private client: SupabaseClient<Database>;
|
40
|
+
|
41
|
+
constructor(supabaseUrl: string, supabaseKey: string) {
|
42
|
+
this.client = createClient<Database>(supabaseUrl, supabaseKey);
|
43
|
+
}
|
44
|
+
|
45
|
+
async getUserDataFromApiKey(apiKey: string): Promise<UserData | undefined> {
|
46
|
+
const { data, error } = await this.client
|
47
|
+
.from("api_keys")
|
48
|
+
.select("user_uuid, users ( nickname )")
|
49
|
+
.eq("api_key", apiKey)
|
50
|
+
.maybeSingle<{
|
51
|
+
user_uuid: supabase.Tables<"api_keys">["user_uuid"];
|
52
|
+
users: {
|
53
|
+
nickname: supabase.Tables<"users">["nickname"];
|
54
|
+
};
|
55
|
+
}>();
|
56
|
+
|
57
|
+
logger.debug(
|
58
|
+
`[getUserDataFromApiKey]: got ${JSON.stringify({ data, error })}`
|
59
|
+
);
|
60
|
+
|
61
|
+
if (error) {
|
62
|
+
throw error;
|
63
|
+
}
|
64
|
+
|
65
|
+
if (data === null) {
|
66
|
+
return undefined;
|
67
|
+
}
|
68
|
+
|
69
|
+
return {
|
70
|
+
user_uuid: data.user_uuid,
|
71
|
+
nickname: data.users.nickname || `user ${data.user_uuid}`,
|
72
|
+
};
|
73
|
+
}
|
74
|
+
|
75
|
+
async createUser(
|
76
|
+
user_uuid: string,
|
77
|
+
email: string,
|
78
|
+
nickname: string,
|
79
|
+
timezone?: string
|
80
|
+
): Promise<void> {
|
81
|
+
const payload: supabase.TablesInsert<"users"> = {
|
82
|
+
uuid: user_uuid,
|
83
|
+
email,
|
84
|
+
nickname,
|
85
|
+
timezone: timezone || "UTC",
|
86
|
+
};
|
87
|
+
|
88
|
+
const { error } = await this.client.from("users").insert(payload);
|
89
|
+
if (error) {
|
90
|
+
throw error;
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
async addApiKey(
|
95
|
+
user_uuid: string,
|
96
|
+
api_key: string,
|
97
|
+
name: string,
|
98
|
+
scopes: string[],
|
99
|
+
is_default: boolean = false
|
100
|
+
): Promise<ApiKey> {
|
101
|
+
const payload: supabase.TablesInsert<"api_keys"> = {
|
102
|
+
user_uuid,
|
103
|
+
api_key,
|
104
|
+
name,
|
105
|
+
scopes,
|
106
|
+
is_default,
|
107
|
+
};
|
108
|
+
const { data, error } = await this.client
|
109
|
+
.from("api_keys")
|
110
|
+
.insert(payload)
|
111
|
+
.select("*")
|
112
|
+
.maybeSingle();
|
113
|
+
if (error) {
|
114
|
+
throw error;
|
115
|
+
}
|
116
|
+
|
117
|
+
return data;
|
118
|
+
}
|
119
|
+
|
120
|
+
async getSavedAgentProfileById(
|
121
|
+
agentProfileId: string
|
122
|
+
): Promise<SavedAgentProfile | undefined> {
|
123
|
+
const { data, error } = await this.client
|
124
|
+
.from("agent_profiles")
|
125
|
+
.select("*")
|
126
|
+
.eq("uuid", agentProfileId)
|
127
|
+
.maybeSingle<supabase.Tables<"agent_profiles">>();
|
128
|
+
if (error) {
|
129
|
+
throw error;
|
130
|
+
}
|
131
|
+
|
132
|
+
return data
|
133
|
+
? SavedAgentProfile.fromJSONObj(data as Record<string, unknown>)
|
134
|
+
: undefined;
|
135
|
+
}
|
136
|
+
|
137
|
+
async getSavedAgentProfileByName(
|
138
|
+
user_uuid: string,
|
139
|
+
agentProfileName: string
|
140
|
+
): Promise<SavedAgentProfile | undefined> {
|
141
|
+
const { data, error } = await this.client
|
142
|
+
.from("agent_profiles")
|
143
|
+
.select("*")
|
144
|
+
.eq("user_uuid", user_uuid)
|
145
|
+
.eq("profile_name", agentProfileName)
|
146
|
+
.maybeSingle<supabase.Tables<"agent_profiles">>();
|
147
|
+
if (error) {
|
148
|
+
throw error;
|
149
|
+
}
|
150
|
+
|
151
|
+
return data
|
152
|
+
? SavedAgentProfile.fromJSONObj(data as Record<string, unknown>)
|
153
|
+
: undefined;
|
154
|
+
}
|
155
|
+
|
156
|
+
async getAgentProfileById(
|
157
|
+
agentProfileId: string
|
158
|
+
): Promise<AgentProfile | undefined> {
|
159
|
+
const { data, error } = await this.client
|
160
|
+
.from("agent_profiles")
|
161
|
+
.select("profile")
|
162
|
+
.eq("uuid", agentProfileId)
|
163
|
+
.maybeSingle<{
|
164
|
+
profile: supabase.Tables<"agent_profiles">["profile"];
|
165
|
+
}>();
|
166
|
+
if (error) {
|
167
|
+
throw error;
|
168
|
+
}
|
169
|
+
return data
|
170
|
+
? AgentProfile.fromJSONObj(data.profile as Record<string, unknown>)
|
171
|
+
: undefined;
|
172
|
+
}
|
173
|
+
|
174
|
+
async createAgentProfile(
|
175
|
+
user_uuid: string,
|
176
|
+
profileName: string,
|
177
|
+
profile: AgentProfile
|
178
|
+
): Promise<string | undefined> {
|
179
|
+
const payload: supabase.TablesInsert<"agent_profiles"> = {
|
180
|
+
profile: profile as unknown as supabase.Json,
|
181
|
+
user_uuid,
|
182
|
+
profile_name: profileName,
|
183
|
+
};
|
184
|
+
const { data, error } = await this.client
|
185
|
+
.from("agent_profiles")
|
186
|
+
.upsert(payload)
|
187
|
+
.select("uuid");
|
188
|
+
if (error) {
|
189
|
+
throw error;
|
190
|
+
}
|
191
|
+
|
192
|
+
if (!data || !data[0] || !data[0].uuid) {
|
193
|
+
return undefined;
|
194
|
+
}
|
195
|
+
|
196
|
+
return data[0].uuid;
|
197
|
+
}
|
198
|
+
|
199
|
+
async clearAgentProfiles(): Promise<void> {
|
200
|
+
await this.client.from("agent_profiles").delete().neq("uuid", "");
|
201
|
+
}
|
202
|
+
|
203
|
+
// TODO: is there a session model?
|
204
|
+
|
205
|
+
async getSessionById(
|
206
|
+
session_uuid: string
|
207
|
+
): Promise<supabase.Tables<"sessions"> | undefined> {
|
208
|
+
const { data, error } = await this.client
|
209
|
+
.from("sessions")
|
210
|
+
.select("*")
|
211
|
+
.eq("uuid", session_uuid)
|
212
|
+
.maybeSingle();
|
213
|
+
if (error) {
|
214
|
+
throw error;
|
215
|
+
}
|
216
|
+
return data;
|
217
|
+
}
|
218
|
+
|
219
|
+
async getSessionByName(
|
220
|
+
user_uuid: string,
|
221
|
+
session_name: string
|
222
|
+
): Promise<supabase.Tables<"sessions"> | undefined> {
|
223
|
+
const { data, error } = await this.client
|
224
|
+
.from("sessions")
|
225
|
+
.select("*")
|
226
|
+
.eq("user_uuid", user_uuid)
|
227
|
+
.eq("title", session_name)
|
228
|
+
.maybeSingle();
|
229
|
+
if (error) {
|
230
|
+
throw error;
|
231
|
+
}
|
232
|
+
return data;
|
233
|
+
}
|
234
|
+
|
235
|
+
async createSession(
|
236
|
+
user_uuid: string,
|
237
|
+
title: string,
|
238
|
+
agentProfileId: string
|
239
|
+
): Promise<string | undefined> {
|
240
|
+
const payload: supabase.TablesInsert<"sessions"> = {
|
241
|
+
agent_profile_uuid: agentProfileId,
|
242
|
+
user_uuid,
|
243
|
+
title: title,
|
244
|
+
conversation: [],
|
245
|
+
};
|
246
|
+
const { data, error } = await this.client
|
247
|
+
.from("sessions")
|
248
|
+
.upsert(payload)
|
249
|
+
.select("uuid");
|
250
|
+
if (error) {
|
251
|
+
throw error;
|
252
|
+
}
|
253
|
+
|
254
|
+
if (!data || !data[0] || !data[0].uuid) {
|
255
|
+
return undefined;
|
256
|
+
}
|
257
|
+
|
258
|
+
return data[0].uuid;
|
259
|
+
}
|
260
|
+
|
261
|
+
async clearSessions(): Promise<void> {
|
262
|
+
await this.client.from("sessions").delete().neq("uuid", "");
|
263
|
+
}
|
264
|
+
}
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import {
|
2
|
+
ChatCompletionMessageParam,
|
3
|
+
ChatCompletionAssistantMessageParam,
|
4
|
+
ChatCompletionToolMessageParam,
|
5
|
+
ChatCompletionMessageToolCall,
|
6
|
+
} from "openai/resources.mjs";
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Message from a user to the server
|
10
|
+
*/
|
11
|
+
export type ClientUserMessage = {
|
12
|
+
type: "msg";
|
13
|
+
message: string;
|
14
|
+
};
|
15
|
+
|
16
|
+
/**
|
17
|
+
* (from server) Chat history
|
18
|
+
*/
|
19
|
+
export type ServerHistory = {
|
20
|
+
type: "history";
|
21
|
+
/// Conversation history in the form we expect. The `name` attribute of
|
22
|
+
/// `ChatCompletionUserMessageParam` (role: "user") holds the original
|
23
|
+
/// message sender.
|
24
|
+
messages: ChatCompletionMessageParam[];
|
25
|
+
};
|
26
|
+
|
27
|
+
export type ServerUserJoined = {
|
28
|
+
type: "user_joined";
|
29
|
+
user: string;
|
30
|
+
};
|
31
|
+
|
32
|
+
export type ServerUserLeft = {
|
33
|
+
type: "user_left";
|
34
|
+
user: string;
|
35
|
+
};
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Message from the server, informing us of a message in the chat history
|
39
|
+
*/
|
40
|
+
export type ServerUserMessage = {
|
41
|
+
type: "user_msg";
|
42
|
+
message_id: string;
|
43
|
+
message: string;
|
44
|
+
from: string;
|
45
|
+
};
|
46
|
+
|
47
|
+
export type ServerAgentMessage = {
|
48
|
+
type: "agent_msg";
|
49
|
+
message_id: string;
|
50
|
+
message: ChatCompletionAssistantMessageParam;
|
51
|
+
};
|
52
|
+
|
53
|
+
export type ServerAgentMessageChunk = {
|
54
|
+
type: "agent_msg_chunk";
|
55
|
+
message_id: string;
|
56
|
+
message: string;
|
57
|
+
end: boolean;
|
58
|
+
};
|
59
|
+
|
60
|
+
/**
|
61
|
+
* For information only (to keep the chat window consistent).
|
62
|
+
*/
|
63
|
+
export type ServerToolCall = {
|
64
|
+
type: "agent_tool_call";
|
65
|
+
message_id: string;
|
66
|
+
message: ChatCompletionMessageToolCall;
|
67
|
+
};
|
68
|
+
|
69
|
+
/**
|
70
|
+
* For information only (to keep the chat window consistent)
|
71
|
+
*/
|
72
|
+
export type ServerToolCallResult = {
|
73
|
+
type: "tool_call_result";
|
74
|
+
message_id: string;
|
75
|
+
message: ChatCompletionToolMessageParam;
|
76
|
+
};
|
77
|
+
|
78
|
+
export type ServerTyping = {
|
79
|
+
type: "typing";
|
80
|
+
from: string;
|
81
|
+
};
|
82
|
+
|
83
|
+
export type ClientToServer = ClientUserMessage;
|
84
|
+
|
85
|
+
export type ServerToClient =
|
86
|
+
| ServerUserJoined
|
87
|
+
| ServerUserLeft
|
88
|
+
| ServerUserMessage
|
89
|
+
| ServerAgentMessage
|
90
|
+
| ServerAgentMessageChunk
|
91
|
+
| ServerTyping;
|
@@ -0,0 +1,177 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
// -*- typescript -*-
|
3
|
+
|
4
|
+
import type * as supabase from "../../../supabase/database.types";
|
5
|
+
import * as dotenv from "dotenv";
|
6
|
+
import { getLogger } from "@xalia/xmcp/sdk";
|
7
|
+
import * as ws from "ws";
|
8
|
+
import { IncomingMessage } from "http";
|
9
|
+
import { parse } from "url";
|
10
|
+
import { ConversationManager } from "./conversationManager";
|
11
|
+
import { Database, UserData, resolveCompoundName } from "./db";
|
12
|
+
import { ApiKeyManager } from "./apiKeyManager";
|
13
|
+
import { ParsedUrlQuery } from "querystring";
|
14
|
+
|
15
|
+
dotenv.config();
|
16
|
+
|
17
|
+
const logger = getLogger();
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Try as id, then by user/name and return the AgentProfile uuid.
|
21
|
+
*/
|
22
|
+
async function resolveAgentProfileId(
|
23
|
+
db: Database,
|
24
|
+
userData: UserData,
|
25
|
+
agentProfileIdentifier: string
|
26
|
+
): Promise<string | undefined> {
|
27
|
+
let ap = await db.getSavedAgentProfileById(agentProfileIdentifier);
|
28
|
+
logger.debug(`[resolveAgentProfileId]: by id: {JSON.stringify(ap)}`);
|
29
|
+
if (ap) {
|
30
|
+
return agentProfileIdentifier;
|
31
|
+
}
|
32
|
+
|
33
|
+
ap = await db.getSavedAgentProfileByName(
|
34
|
+
userData.user_uuid,
|
35
|
+
agentProfileIdentifier
|
36
|
+
);
|
37
|
+
logger.debug(`[resolveAgentProfileId]: by name: {JSON.stringify(ap)}`);
|
38
|
+
if (ap) {
|
39
|
+
return ap.uuid;
|
40
|
+
}
|
41
|
+
|
42
|
+
logger.debug("[resolveAgentProfileId]: agent profile not found");
|
43
|
+
return undefined;
|
44
|
+
}
|
45
|
+
|
46
|
+
async function resolveSessionIdFromIdentifier(
|
47
|
+
db: Database,
|
48
|
+
userData: UserData,
|
49
|
+
sessionIdentifier: string
|
50
|
+
): Promise<string | undefined> {
|
51
|
+
let session: supabase.Tables<"sessions"> | undefined = undefined;
|
52
|
+
const compound = resolveCompoundName(sessionIdentifier);
|
53
|
+
if (typeof compound === "string") {
|
54
|
+
// Interpret as an id, or as a number under the current user.
|
55
|
+
session = await db.getSessionById(compound);
|
56
|
+
if (!session) {
|
57
|
+
session = await db.getSessionByName(userData.user_uuid, compound);
|
58
|
+
}
|
59
|
+
} else {
|
60
|
+
session = await db.getSessionByName(compound[0], compound[1]);
|
61
|
+
}
|
62
|
+
|
63
|
+
return session?.uuid;
|
64
|
+
}
|
65
|
+
|
66
|
+
/**
|
67
|
+
* Expect parameters to be either:
|
68
|
+
* session_id - uuid, name, user/name
|
69
|
+
* agent_profile_id (optional) - uuid, name
|
70
|
+
*
|
71
|
+
* If session_idd resolves to an existing session, it is used (and
|
72
|
+
* agent_profile_id is ignored). Otherwise, session_id is used as the name of
|
73
|
+
* a new session to create using `agent_profile_id`. The session UUID is
|
74
|
+
* returned in either case.
|
75
|
+
*/
|
76
|
+
async function findOrCreateSession(
|
77
|
+
db: Database,
|
78
|
+
userData: UserData,
|
79
|
+
query: ParsedUrlQuery
|
80
|
+
): Promise<string | undefined> {
|
81
|
+
logger.debug(`[findOrCreateSession]: query: ${JSON.stringify(query)}`);
|
82
|
+
if (!query.session_id || typeof query.session_id !== "string") {
|
83
|
+
throw "session_id invalid or not present";
|
84
|
+
}
|
85
|
+
|
86
|
+
const sessionId = await resolveSessionIdFromIdentifier(
|
87
|
+
db,
|
88
|
+
userData,
|
89
|
+
query.session_id
|
90
|
+
);
|
91
|
+
if (sessionId) {
|
92
|
+
return sessionId;
|
93
|
+
}
|
94
|
+
|
95
|
+
const agentProfileIdParam = query.agent_profile_id;
|
96
|
+
logger.debug(`[findOrCreateSession]: agent: ${agentProfileIdParam}`);
|
97
|
+
if (!agentProfileIdParam) {
|
98
|
+
throw "no existing session, and no agent_profile_id given";
|
99
|
+
}
|
100
|
+
if (typeof agentProfileIdParam !== "string") {
|
101
|
+
throw "no existing session and invalid agent_profile_id";
|
102
|
+
}
|
103
|
+
|
104
|
+
logger.debug(`[findOrCreateSession]: creating session: ${query.session_id}`);
|
105
|
+
const agentProfileId = await resolveAgentProfileId(
|
106
|
+
db,
|
107
|
+
userData,
|
108
|
+
agentProfileIdParam
|
109
|
+
);
|
110
|
+
logger.debug(
|
111
|
+
`[findOrCreateSession]: resolved agentProfileId: ${agentProfileId}`
|
112
|
+
);
|
113
|
+
if (!agentProfileId) {
|
114
|
+
throw `no agent profile: ${agentProfileIdParam}`;
|
115
|
+
}
|
116
|
+
|
117
|
+
return db.createSession(userData.user_uuid, query.session_id, agentProfileId);
|
118
|
+
}
|
119
|
+
|
120
|
+
export async function runServer(
|
121
|
+
port: number,
|
122
|
+
supabaseUrl: string,
|
123
|
+
supabaseKey: string,
|
124
|
+
llmUrl: string,
|
125
|
+
xmcpUrl: string
|
126
|
+
): Promise<ws.Server> {
|
127
|
+
return new Promise((r, _e) => {
|
128
|
+
const wss = new ws.Server({ port });
|
129
|
+
const db = new Database(supabaseUrl, supabaseKey);
|
130
|
+
const apiKeyManager = new ApiKeyManager(db);
|
131
|
+
const cm = new ConversationManager(db, llmUrl, xmcpUrl);
|
132
|
+
|
133
|
+
wss.on("connection", async (ws: ws.WebSocket, req: IncomingMessage) => {
|
134
|
+
try {
|
135
|
+
logger.info(`[server] connection: ${req}`);
|
136
|
+
|
137
|
+
// Check header
|
138
|
+
|
139
|
+
logger.info(`[server] headers: ${JSON.stringify(req.headers)}`);
|
140
|
+
const apiKey = req.headers["sec-websocket-protocol"];
|
141
|
+
if (!apiKey) {
|
142
|
+
throw "empty api key";
|
143
|
+
}
|
144
|
+
|
145
|
+
const userData = await apiKeyManager.verifyApiKey(apiKey);
|
146
|
+
if (!userData) {
|
147
|
+
throw "invalid api key";
|
148
|
+
}
|
149
|
+
|
150
|
+
// Get sessionId
|
151
|
+
|
152
|
+
const { query } = parse(req.url || "", true);
|
153
|
+
const sessionId = await findOrCreateSession(db, userData, query);
|
154
|
+
logger.debug(`resolved session id: ${sessionId}`);
|
155
|
+
if (!sessionId) {
|
156
|
+
throw "failed to find/create session";
|
157
|
+
}
|
158
|
+
|
159
|
+
// Associate the ws, username with the conversation
|
160
|
+
|
161
|
+
await cm.join(sessionId, apiKey, apiKey, userData, ws);
|
162
|
+
} catch (e) {
|
163
|
+
if (typeof e === "string") {
|
164
|
+
logger.warn(`[server]: error: ${e}`);
|
165
|
+
ws.close(4000, e);
|
166
|
+
} else {
|
167
|
+
logger.error(`other error: ${JSON.stringify(e)}`);
|
168
|
+
throw e;
|
169
|
+
}
|
170
|
+
}
|
171
|
+
});
|
172
|
+
|
173
|
+
logger.info(`[server] started: ws://localhost:${port}/`);
|
174
|
+
|
175
|
+
r(wss);
|
176
|
+
});
|
177
|
+
}
|
@@ -0,0 +1,103 @@
|
|
1
|
+
import { expect } from "chai";
|
2
|
+
import { Database, SUPABASE_LOCAL_KEY, SUPABASE_LOCAL_URL } from "../chat/db";
|
3
|
+
import { AgentProfile } from "../agent/agent";
|
4
|
+
import { strict as assert } from "assert";
|
5
|
+
import { ApiClient } from "@xalia/xmcp/sdk";
|
6
|
+
import { LOCAL_SERVER_URL } from "../agent/sudoMcpServerManager";
|
7
|
+
|
8
|
+
function getLocalDB(): Database {
|
9
|
+
return new Database(SUPABASE_LOCAL_URL, SUPABASE_LOCAL_KEY);
|
10
|
+
}
|
11
|
+
|
12
|
+
const AGENT_PROFILE: AgentProfile = {
|
13
|
+
model: undefined,
|
14
|
+
system_prompt: "You are an unhelpful agent",
|
15
|
+
mcp_settings: {},
|
16
|
+
};
|
17
|
+
|
18
|
+
async function createDummyUser(db: Database): Promise<void> {
|
19
|
+
const apiClient = new ApiClient(LOCAL_SERVER_URL, "dummy_key");
|
20
|
+
const user = await apiClient.getUserBrief("dummy_user");
|
21
|
+
if (!user) {
|
22
|
+
await db.createUser("dummy_user", "a@b.com", "Dummy User");
|
23
|
+
await db.addApiKey("dummy_user", "dummy_key", "default", [], true);
|
24
|
+
if (!apiClient.getUserBrief("dummy_user")) {
|
25
|
+
throw "unable to create dummy user";
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
describe("DB", () => {
|
31
|
+
it("should get existing user", async function () {
|
32
|
+
const db = getLocalDB();
|
33
|
+
await createDummyUser(db);
|
34
|
+
const dummyUser = await db.getUserDataFromApiKey("dummy_key");
|
35
|
+
expect(dummyUser).eql({
|
36
|
+
user_uuid: "dummy_user",
|
37
|
+
nickname: "Dummy User",
|
38
|
+
});
|
39
|
+
});
|
40
|
+
|
41
|
+
it("should return undefined for non-existant user", async function () {
|
42
|
+
const db = getLocalDB();
|
43
|
+
const dummyUser = await db.getUserDataFromApiKey("no_such_key");
|
44
|
+
expect(dummyUser).to.equal(undefined);
|
45
|
+
});
|
46
|
+
|
47
|
+
it("should create and retrieve agent profiles", async function () {
|
48
|
+
const db = getLocalDB();
|
49
|
+
|
50
|
+
await db.clearAgentProfiles();
|
51
|
+
const agentProfileId = await db.createAgentProfile(
|
52
|
+
"dummy_user",
|
53
|
+
"test_profile",
|
54
|
+
AGENT_PROFILE
|
55
|
+
);
|
56
|
+
expect(agentProfileId).to.not.equal(undefined);
|
57
|
+
assert(agentProfileId);
|
58
|
+
|
59
|
+
const savedAgentProfile =
|
60
|
+
(await db.getSavedAgentProfileById(agentProfileId))!;
|
61
|
+
const savedAgentProfileByName = (await db.getSavedAgentProfileByName(
|
62
|
+
"dummy_user",
|
63
|
+
"test_profile"
|
64
|
+
))!;
|
65
|
+
const agentProfile = (await db.getAgentProfileById(agentProfileId))!;
|
66
|
+
|
67
|
+
expect(savedAgentProfile.uuid).eql(agentProfileId);
|
68
|
+
expect(savedAgentProfile.profile).eql(AGENT_PROFILE);
|
69
|
+
expect(savedAgentProfile?.user_uuid).eql("dummy_user");
|
70
|
+
expect(savedAgentProfile?.profile_name).eql("test_profile");
|
71
|
+
|
72
|
+
expect(savedAgentProfileByName).eql(savedAgentProfile);
|
73
|
+
|
74
|
+
expect(agentProfile).eql(AGENT_PROFILE);
|
75
|
+
});
|
76
|
+
|
77
|
+
it("should create and retrieve sessions", async function () {
|
78
|
+
const db = getLocalDB();
|
79
|
+
|
80
|
+
await db.clearAgentProfiles();
|
81
|
+
await db.clearSessions();
|
82
|
+
const agentProfileId = await db.createAgentProfile(
|
83
|
+
"dummy_user",
|
84
|
+
"test_profile",
|
85
|
+
AGENT_PROFILE
|
86
|
+
);
|
87
|
+
assert(agentProfileId);
|
88
|
+
|
89
|
+
const sessionId = await db.createSession(
|
90
|
+
"dummy_user",
|
91
|
+
"test_session",
|
92
|
+
agentProfileId
|
93
|
+
);
|
94
|
+
assert(sessionId);
|
95
|
+
const session = (await db.getSessionById(sessionId))!;
|
96
|
+
|
97
|
+
expect(session.agent_profile_uuid).eql(agentProfileId);
|
98
|
+
expect(session.conversation).eql([]);
|
99
|
+
expect(session.title).eql("test_session");
|
100
|
+
expect(session.user_uuid).eql("dummy_user");
|
101
|
+
expect(session.uuid).eql(sessionId);
|
102
|
+
});
|
103
|
+
});
|
package/src/test/prompt.test.ts
CHANGED
@@ -1,20 +1,16 @@
|
|
1
1
|
import { expect } from "chai";
|
2
|
-
import { McpServerManager } from "../mcpServerManager";
|
3
|
-
import {
|
4
|
-
SudoMcpServerManager,
|
5
|
-
LOCAL_SERVER_URL,
|
6
|
-
} from "../sudoMcpServerManager";
|
7
2
|
import chalk from "chalk";
|
8
3
|
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
9
4
|
|
10
|
-
|
5
|
+
import { McpServerManager } from "../agent/mcpServerManager";
|
6
|
+
import { SkillManager, LOCAL_SERVER_URL } from "../agent/sudoMcpServerManager";
|
11
7
|
|
12
|
-
|
13
|
-
|
14
|
-
> {
|
8
|
+
let managers: [McpServerManager, SkillManager] | undefined = undefined;
|
9
|
+
|
10
|
+
async function getServerManagers(): Promise<[McpServerManager, SkillManager]> {
|
15
11
|
if (!managers) {
|
16
12
|
const tm = new McpServerManager();
|
17
|
-
const sm = await
|
13
|
+
const sm = await SkillManager.initialize(
|
18
14
|
tm,
|
19
15
|
(_url: string) => {
|
20
16
|
throw "unexpected call to openUrl";
|