@harborclient/team-hub 0.2.3 → 0.3.0
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/dist/cli.js +420 -13
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -25,6 +25,35 @@ import path from "path";
|
|
|
25
25
|
import { parse as parseYaml } from "yaml";
|
|
26
26
|
|
|
27
27
|
// src/config/llmConfig.ts
|
|
28
|
+
function headerRowsFromSingleKeyObject(record) {
|
|
29
|
+
const rows = [];
|
|
30
|
+
for (const [key, value] of Object.entries(record)) {
|
|
31
|
+
const trimmedKey = key.trim();
|
|
32
|
+
if (trimmedKey.length > 0) {
|
|
33
|
+
rows.push({ key: trimmedKey, value: String(value) });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
function normalizeHubMcpHeaders(headers) {
|
|
39
|
+
if (!headers) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(headers)) {
|
|
43
|
+
return headerRowsFromSingleKeyObject(headers);
|
|
44
|
+
}
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (const item of headers) {
|
|
47
|
+
if (Array.isArray(item)) {
|
|
48
|
+
for (const nested of item) {
|
|
49
|
+
rows.push(...headerRowsFromSingleKeyObject(nested));
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
rows.push(...headerRowsFromSingleKeyObject(item));
|
|
54
|
+
}
|
|
55
|
+
return rows;
|
|
56
|
+
}
|
|
28
57
|
function normalizeLlmConfig(section) {
|
|
29
58
|
const providers = {};
|
|
30
59
|
if (section.providers.openai?.apiKey) {
|
|
@@ -36,9 +65,15 @@ function normalizeLlmConfig(section) {
|
|
|
36
65
|
if (section.providers.gemini?.apiKey) {
|
|
37
66
|
providers.gemini = { apiKey: section.providers.gemini.apiKey };
|
|
38
67
|
}
|
|
68
|
+
const mcp = section.mcp && section.mcp.length > 0 ? section.mcp.map((entry) => ({
|
|
69
|
+
name: entry.name.trim(),
|
|
70
|
+
url: entry.url.trim().replace(/\/+$/, ""),
|
|
71
|
+
headers: normalizeHubMcpHeaders(entry.headers)
|
|
72
|
+
})) : void 0;
|
|
39
73
|
return {
|
|
40
74
|
providers,
|
|
41
|
-
...section.models && section.models.length > 0 ? { models: section.models } : {}
|
|
75
|
+
...section.models && section.models.length > 0 ? { models: section.models } : {},
|
|
76
|
+
...mcp && mcp.length > 0 ? { mcp } : {}
|
|
42
77
|
};
|
|
43
78
|
}
|
|
44
79
|
|
|
@@ -116,6 +151,15 @@ var redisSectionSchema = z.object({
|
|
|
116
151
|
var llmProviderEntrySchema = z.object({
|
|
117
152
|
apiKey: z.string().trim().min(1, { message: "LLM provider apiKey must not be empty." })
|
|
118
153
|
});
|
|
154
|
+
var hubMcpHeadersSchema = z.union([
|
|
155
|
+
z.record(z.string(), z.string()),
|
|
156
|
+
z.array(z.union([z.record(z.string(), z.string()), z.array(z.record(z.string(), z.string()))]))
|
|
157
|
+
]);
|
|
158
|
+
var hubMcpServerEntrySchema = z.object({
|
|
159
|
+
name: z.string().trim().min(1),
|
|
160
|
+
url: z.string().trim().min(1),
|
|
161
|
+
headers: hubMcpHeadersSchema.optional()
|
|
162
|
+
});
|
|
119
163
|
var llmSectionSchema = z.object({
|
|
120
164
|
providers: z.object({
|
|
121
165
|
openai: llmProviderEntrySchema.optional(),
|
|
@@ -125,7 +169,8 @@ var llmSectionSchema = z.object({
|
|
|
125
169
|
(providers) => Boolean(providers.openai?.apiKey || providers.claude?.apiKey || providers.gemini?.apiKey),
|
|
126
170
|
{ message: "llm.providers must include at least one provider with an apiKey." }
|
|
127
171
|
),
|
|
128
|
-
models: z.array(z.string().trim().min(1)).optional()
|
|
172
|
+
models: z.array(z.string().trim().min(1)).optional(),
|
|
173
|
+
mcp: z.array(hubMcpServerEntrySchema).optional()
|
|
129
174
|
});
|
|
130
175
|
var pluginsSectionSchema = z.object({
|
|
131
176
|
catalogs: z.array(z.string().trim().url()).optional(),
|
|
@@ -6043,6 +6088,12 @@ function canAccessEnvironment(user, environmentId) {
|
|
|
6043
6088
|
}
|
|
6044
6089
|
return user.environmentAccess.includes(environmentId);
|
|
6045
6090
|
}
|
|
6091
|
+
function canDeleteCollection(user, collection) {
|
|
6092
|
+
return canUseDataApi(user) && canAccessCollection(user, collection.id) && collection.createdByUserId === user.id && !collection.deletionLocked;
|
|
6093
|
+
}
|
|
6094
|
+
function canDeleteRequest(user, request) {
|
|
6095
|
+
return canUseDataApi(user) && canAccessCollection(user, request.collectionId) && request.createdByUserId === user.id;
|
|
6096
|
+
}
|
|
6046
6097
|
function canCreateCollection(user) {
|
|
6047
6098
|
return user.role === "user" && hasWildcardAccess(user.collectionAccess);
|
|
6048
6099
|
}
|
|
@@ -6718,6 +6769,80 @@ async function registerAdminRoutes(app, options) {
|
|
|
6718
6769
|
}
|
|
6719
6770
|
}
|
|
6720
6771
|
});
|
|
6772
|
+
routes.route({
|
|
6773
|
+
method: "GET",
|
|
6774
|
+
url: "/admin/collections/:collectionId/folders",
|
|
6775
|
+
schema: {
|
|
6776
|
+
params: collectionIdParamSchema,
|
|
6777
|
+
response: {
|
|
6778
|
+
200: listFoldersResponseSchema,
|
|
6779
|
+
403: errorResponseSchema,
|
|
6780
|
+
404: errorResponseSchema
|
|
6781
|
+
}
|
|
6782
|
+
},
|
|
6783
|
+
/**
|
|
6784
|
+
* Lists folders in a collection for operator inspection.
|
|
6785
|
+
*/
|
|
6786
|
+
handler: async (request, reply) => {
|
|
6787
|
+
try {
|
|
6788
|
+
const user = requireAuthenticatedUser(request);
|
|
6789
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
6790
|
+
return;
|
|
6791
|
+
}
|
|
6792
|
+
const collection = await db.findCollectionById(request.params.collectionId);
|
|
6793
|
+
if (!collection) {
|
|
6794
|
+
void reply.code(404).send({ error: "Collection not found" });
|
|
6795
|
+
return;
|
|
6796
|
+
}
|
|
6797
|
+
const folders = await db.listFolders(request.params.collectionId);
|
|
6798
|
+
return reply.send({
|
|
6799
|
+
folders: folders.map((folder) => serializeFolder(folder))
|
|
6800
|
+
});
|
|
6801
|
+
} catch (error) {
|
|
6802
|
+
if (handleDbError(reply, error)) {
|
|
6803
|
+
return;
|
|
6804
|
+
}
|
|
6805
|
+
throw error;
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
6808
|
+
});
|
|
6809
|
+
routes.route({
|
|
6810
|
+
method: "GET",
|
|
6811
|
+
url: "/admin/collections/:collectionId/requests",
|
|
6812
|
+
schema: {
|
|
6813
|
+
params: collectionIdParamSchema,
|
|
6814
|
+
response: {
|
|
6815
|
+
200: listRequestsResponseSchema,
|
|
6816
|
+
403: errorResponseSchema,
|
|
6817
|
+
404: errorResponseSchema
|
|
6818
|
+
}
|
|
6819
|
+
},
|
|
6820
|
+
/**
|
|
6821
|
+
* Lists saved requests in a collection for operator inspection.
|
|
6822
|
+
*/
|
|
6823
|
+
handler: async (request, reply) => {
|
|
6824
|
+
try {
|
|
6825
|
+
const user = requireAuthenticatedUser(request);
|
|
6826
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
6827
|
+
return;
|
|
6828
|
+
}
|
|
6829
|
+
const collection = await db.findCollectionById(request.params.collectionId);
|
|
6830
|
+
if (!collection) {
|
|
6831
|
+
void reply.code(404).send({ error: "Collection not found" });
|
|
6832
|
+
return;
|
|
6833
|
+
}
|
|
6834
|
+
const requests = await db.listRequests(request.params.collectionId);
|
|
6835
|
+
return reply.send({
|
|
6836
|
+
requests: requests.map((savedRequest) => serializeSavedRequest(savedRequest))
|
|
6837
|
+
});
|
|
6838
|
+
} catch (error) {
|
|
6839
|
+
if (handleDbError(reply, error)) {
|
|
6840
|
+
return;
|
|
6841
|
+
}
|
|
6842
|
+
throw error;
|
|
6843
|
+
}
|
|
6844
|
+
}
|
|
6845
|
+
});
|
|
6721
6846
|
routes.route({
|
|
6722
6847
|
method: "GET",
|
|
6723
6848
|
url: "/admin/environments",
|
|
@@ -6787,6 +6912,41 @@ async function registerAdminRoutes(app, options) {
|
|
|
6787
6912
|
}
|
|
6788
6913
|
}
|
|
6789
6914
|
});
|
|
6915
|
+
routes.route({
|
|
6916
|
+
method: "DELETE",
|
|
6917
|
+
url: "/admin/requests/:id",
|
|
6918
|
+
schema: {
|
|
6919
|
+
params: idParamSchema,
|
|
6920
|
+
response: {
|
|
6921
|
+
204: emptyResponseSchema,
|
|
6922
|
+
403: errorResponseSchema,
|
|
6923
|
+
404: errorResponseSchema
|
|
6924
|
+
}
|
|
6925
|
+
},
|
|
6926
|
+
/**
|
|
6927
|
+
* Deletes a saved request regardless of collection access lists.
|
|
6928
|
+
*/
|
|
6929
|
+
handler: async (request, reply) => {
|
|
6930
|
+
try {
|
|
6931
|
+
const user = requireAuthenticatedUser(request);
|
|
6932
|
+
if (denyUnlessAllowed(reply, canUseManagementApi(user))) {
|
|
6933
|
+
return;
|
|
6934
|
+
}
|
|
6935
|
+
const existingRequest = await db.findRequestById(request.params.id);
|
|
6936
|
+
if (!existingRequest) {
|
|
6937
|
+
void reply.code(404).send({ error: "Request not found" });
|
|
6938
|
+
return;
|
|
6939
|
+
}
|
|
6940
|
+
await db.deleteRequest(request.params.id, user.id);
|
|
6941
|
+
return reply.code(204).send(null);
|
|
6942
|
+
} catch (error) {
|
|
6943
|
+
if (handleDbError(reply, error)) {
|
|
6944
|
+
return;
|
|
6945
|
+
}
|
|
6946
|
+
throw error;
|
|
6947
|
+
}
|
|
6948
|
+
}
|
|
6949
|
+
});
|
|
6790
6950
|
routes.route({
|
|
6791
6951
|
method: "PUT",
|
|
6792
6952
|
url: "/admin/collections/:id",
|
|
@@ -7350,12 +7510,6 @@ async function registerCollectionRoutes(app, db) {
|
|
|
7350
7510
|
handler: async (request, reply) => {
|
|
7351
7511
|
try {
|
|
7352
7512
|
const user = requireAuthenticatedUser(request);
|
|
7353
|
-
if (denyUnlessAllowed(
|
|
7354
|
-
reply,
|
|
7355
|
-
canUseDataApi(user) && canAccessCollection(user, request.params.id)
|
|
7356
|
-
)) {
|
|
7357
|
-
return;
|
|
7358
|
-
}
|
|
7359
7513
|
const collection = await db.findCollectionById(request.params.id);
|
|
7360
7514
|
if (!collection) {
|
|
7361
7515
|
void reply.code(404).send({ error: "Collection not found" });
|
|
@@ -7364,6 +7518,9 @@ async function registerCollectionRoutes(app, db) {
|
|
|
7364
7518
|
if (collection.deletionLocked) {
|
|
7365
7519
|
throw new DeletionLockedError("collection");
|
|
7366
7520
|
}
|
|
7521
|
+
if (denyUnlessAllowed(reply, canDeleteCollection(user, collection))) {
|
|
7522
|
+
return;
|
|
7523
|
+
}
|
|
7367
7524
|
await db.deleteCollection(request.params.id, user.id);
|
|
7368
7525
|
return reply.code(204).send(null);
|
|
7369
7526
|
} catch (error) {
|
|
@@ -7892,10 +8049,7 @@ async function registerRequestRoutes(app, db) {
|
|
|
7892
8049
|
if (!existingRequest) {
|
|
7893
8050
|
return reply.code(404).send({ error: "Request not found" });
|
|
7894
8051
|
}
|
|
7895
|
-
if (denyUnlessAllowed(
|
|
7896
|
-
reply,
|
|
7897
|
-
canUseDataApi(user) && canAccessCollection(user, existingRequest.collectionId)
|
|
7898
|
-
)) {
|
|
8052
|
+
if (denyUnlessAllowed(reply, canDeleteRequest(user, existingRequest))) {
|
|
7899
8053
|
return;
|
|
7900
8054
|
}
|
|
7901
8055
|
await db.deleteRequest(request.params.id, user.id);
|
|
@@ -8107,6 +8261,258 @@ async function runLlmCompletion(config, input) {
|
|
|
8107
8261
|
return parseCompletionResponse(json);
|
|
8108
8262
|
}
|
|
8109
8263
|
|
|
8264
|
+
// src/server/llm/hubMcpToolNames.ts
|
|
8265
|
+
var HUB_MCP_TOOL_PREFIX = "hubmcp__";
|
|
8266
|
+
function encodeHubMcpToolName(serverIndex, toolName) {
|
|
8267
|
+
return `${HUB_MCP_TOOL_PREFIX}${serverIndex}__${toolName}`;
|
|
8268
|
+
}
|
|
8269
|
+
function decodeHubMcpToolName(prefixed) {
|
|
8270
|
+
if (!prefixed.startsWith(HUB_MCP_TOOL_PREFIX)) {
|
|
8271
|
+
return null;
|
|
8272
|
+
}
|
|
8273
|
+
const rest = prefixed.slice(HUB_MCP_TOOL_PREFIX.length);
|
|
8274
|
+
const separatorIndex = rest.indexOf("__");
|
|
8275
|
+
if (separatorIndex <= 0) {
|
|
8276
|
+
return null;
|
|
8277
|
+
}
|
|
8278
|
+
const serverIndex = Number.parseInt(rest.slice(0, separatorIndex), 10);
|
|
8279
|
+
if (!Number.isInteger(serverIndex) || serverIndex < 0) {
|
|
8280
|
+
return null;
|
|
8281
|
+
}
|
|
8282
|
+
const toolName = rest.slice(separatorIndex + 2);
|
|
8283
|
+
if (!toolName) {
|
|
8284
|
+
return null;
|
|
8285
|
+
}
|
|
8286
|
+
return { serverIndex, toolName };
|
|
8287
|
+
}
|
|
8288
|
+
function isHubMcpToolName(name) {
|
|
8289
|
+
return decodeHubMcpToolName(name) !== null;
|
|
8290
|
+
}
|
|
8291
|
+
|
|
8292
|
+
// src/server/llm/mcpClient.ts
|
|
8293
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8294
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
8295
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
8296
|
+
var connectedClients = /* @__PURE__ */ new Map();
|
|
8297
|
+
var activeConfigSignature = "";
|
|
8298
|
+
function buildRequestHeaders(headers) {
|
|
8299
|
+
const result = {};
|
|
8300
|
+
for (const row of headers) {
|
|
8301
|
+
if (row.key.trim()) {
|
|
8302
|
+
result[row.key.trim()] = row.value;
|
|
8303
|
+
}
|
|
8304
|
+
}
|
|
8305
|
+
return result;
|
|
8306
|
+
}
|
|
8307
|
+
function buildMcpConfigSignature(config) {
|
|
8308
|
+
return JSON.stringify(config.mcp ?? []);
|
|
8309
|
+
}
|
|
8310
|
+
function flattenMcpToolContent(content) {
|
|
8311
|
+
if (!Array.isArray(content)) {
|
|
8312
|
+
return JSON.stringify(content ?? null);
|
|
8313
|
+
}
|
|
8314
|
+
const parts = content.map((item) => {
|
|
8315
|
+
if (!item || typeof item !== "object") {
|
|
8316
|
+
return String(item);
|
|
8317
|
+
}
|
|
8318
|
+
const record = item;
|
|
8319
|
+
if (record.type === "text" && typeof record.text === "string") {
|
|
8320
|
+
return record.text;
|
|
8321
|
+
}
|
|
8322
|
+
if (record.type === "resource" && typeof record.uri === "string") {
|
|
8323
|
+
return record.uri;
|
|
8324
|
+
}
|
|
8325
|
+
return JSON.stringify(record);
|
|
8326
|
+
});
|
|
8327
|
+
return parts.join("\n");
|
|
8328
|
+
}
|
|
8329
|
+
function toOpenAiTool(serverIndex, tool) {
|
|
8330
|
+
return {
|
|
8331
|
+
type: "function",
|
|
8332
|
+
function: {
|
|
8333
|
+
name: encodeHubMcpToolName(serverIndex, tool.name),
|
|
8334
|
+
description: tool.description ?? `MCP tool ${tool.name}`,
|
|
8335
|
+
parameters: tool.inputSchema ?? {
|
|
8336
|
+
type: "object",
|
|
8337
|
+
properties: {},
|
|
8338
|
+
additionalProperties: true
|
|
8339
|
+
}
|
|
8340
|
+
}
|
|
8341
|
+
};
|
|
8342
|
+
}
|
|
8343
|
+
async function connectHubMcpServer(serverIndex, server) {
|
|
8344
|
+
const headers = buildRequestHeaders(server.headers);
|
|
8345
|
+
const url = new URL(server.url);
|
|
8346
|
+
const client = new Client(
|
|
8347
|
+
{
|
|
8348
|
+
name: "team-hub",
|
|
8349
|
+
version: "1.0.0"
|
|
8350
|
+
},
|
|
8351
|
+
{}
|
|
8352
|
+
);
|
|
8353
|
+
let transport;
|
|
8354
|
+
try {
|
|
8355
|
+
transport = new StreamableHTTPClientTransport(url, {
|
|
8356
|
+
requestInit: { headers }
|
|
8357
|
+
});
|
|
8358
|
+
await client.connect(transport);
|
|
8359
|
+
} catch {
|
|
8360
|
+
transport = new SSEClientTransport(url, {
|
|
8361
|
+
requestInit: { headers }
|
|
8362
|
+
});
|
|
8363
|
+
await client.connect(transport);
|
|
8364
|
+
}
|
|
8365
|
+
const { tools } = await client.listTools();
|
|
8366
|
+
return {
|
|
8367
|
+
serverIndex,
|
|
8368
|
+
client,
|
|
8369
|
+
remoteTools: tools
|
|
8370
|
+
};
|
|
8371
|
+
}
|
|
8372
|
+
async function ensureHubMcpConnections(config) {
|
|
8373
|
+
const servers = config.mcp ?? [];
|
|
8374
|
+
const signature = buildMcpConfigSignature(config);
|
|
8375
|
+
if (signature !== activeConfigSignature) {
|
|
8376
|
+
await disposeHubMcpConnections();
|
|
8377
|
+
activeConfigSignature = signature;
|
|
8378
|
+
}
|
|
8379
|
+
if (servers.length === 0) {
|
|
8380
|
+
return;
|
|
8381
|
+
}
|
|
8382
|
+
for (const [index, server] of servers.entries()) {
|
|
8383
|
+
if (connectedClients.has(index)) {
|
|
8384
|
+
continue;
|
|
8385
|
+
}
|
|
8386
|
+
try {
|
|
8387
|
+
const connected = await connectHubMcpServer(index, server);
|
|
8388
|
+
connectedClients.set(index, connected);
|
|
8389
|
+
} catch (error) {
|
|
8390
|
+
const message = error instanceof Error ? error.message : "Connection failed";
|
|
8391
|
+
console.warn(`Hub MCP server "${server.name}" failed to connect: ${message}`);
|
|
8392
|
+
}
|
|
8393
|
+
}
|
|
8394
|
+
}
|
|
8395
|
+
function listHubMcpTools() {
|
|
8396
|
+
const tools = [];
|
|
8397
|
+
for (const entry of connectedClients.values()) {
|
|
8398
|
+
for (const tool of entry.remoteTools) {
|
|
8399
|
+
tools.push(toOpenAiTool(entry.serverIndex, tool));
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
return tools;
|
|
8403
|
+
}
|
|
8404
|
+
async function callHubMcpTool(prefixedName, args) {
|
|
8405
|
+
const decoded = decodeHubMcpToolName(prefixedName);
|
|
8406
|
+
if (!decoded) {
|
|
8407
|
+
return JSON.stringify({ error: `Unknown hub MCP tool: ${prefixedName}` });
|
|
8408
|
+
}
|
|
8409
|
+
const entry = connectedClients.get(decoded.serverIndex);
|
|
8410
|
+
if (!entry) {
|
|
8411
|
+
return JSON.stringify({ error: `Hub MCP server ${decoded.serverIndex} is not connected.` });
|
|
8412
|
+
}
|
|
8413
|
+
try {
|
|
8414
|
+
const result = await entry.client.callTool({
|
|
8415
|
+
name: decoded.toolName,
|
|
8416
|
+
arguments: args ?? {}
|
|
8417
|
+
});
|
|
8418
|
+
if (result.isError) {
|
|
8419
|
+
return JSON.stringify({
|
|
8420
|
+
error: flattenMcpToolContent(result.content)
|
|
8421
|
+
});
|
|
8422
|
+
}
|
|
8423
|
+
return flattenMcpToolContent(result.content);
|
|
8424
|
+
} catch (error) {
|
|
8425
|
+
const message = error instanceof Error ? error.message : "MCP tool execution failed.";
|
|
8426
|
+
return JSON.stringify({ error: message });
|
|
8427
|
+
}
|
|
8428
|
+
}
|
|
8429
|
+
async function disposeHubMcpConnections() {
|
|
8430
|
+
const closePromises = [...connectedClients.values()].map(async (entry) => {
|
|
8431
|
+
try {
|
|
8432
|
+
await entry.client.close();
|
|
8433
|
+
} catch {
|
|
8434
|
+
}
|
|
8435
|
+
});
|
|
8436
|
+
connectedClients.clear();
|
|
8437
|
+
activeConfigSignature = "";
|
|
8438
|
+
await Promise.allSettled(closePromises);
|
|
8439
|
+
}
|
|
8440
|
+
|
|
8441
|
+
// src/server/llm/agent.ts
|
|
8442
|
+
var HUB_CHAT_STEP_MAX_ITERATIONS = 8;
|
|
8443
|
+
function addUsage(current, next) {
|
|
8444
|
+
return {
|
|
8445
|
+
promptTokens: current.promptTokens + next.promptTokens,
|
|
8446
|
+
completionTokens: current.completionTokens + next.completionTokens,
|
|
8447
|
+
totalTokens: current.totalTokens + next.totalTokens
|
|
8448
|
+
};
|
|
8449
|
+
}
|
|
8450
|
+
function parseToolArguments(raw) {
|
|
8451
|
+
try {
|
|
8452
|
+
return JSON.parse(raw);
|
|
8453
|
+
} catch {
|
|
8454
|
+
return {};
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
8457
|
+
async function runHubChatStep(config, input, deps = {}) {
|
|
8458
|
+
const runCompletion = deps.runCompletion ?? runLlmCompletion;
|
|
8459
|
+
const ensureConnections = deps.ensureConnections ?? ensureHubMcpConnections;
|
|
8460
|
+
const listTools = deps.listTools ?? listHubMcpTools;
|
|
8461
|
+
const callTool = deps.callTool ?? callHubMcpTool;
|
|
8462
|
+
await ensureConnections(config);
|
|
8463
|
+
const hubTools = listTools();
|
|
8464
|
+
const mergedTools = hubTools.length > 0 || (input.tools?.length ?? 0) > 0 ? [...hubTools, ...input.tools ?? []] : void 0;
|
|
8465
|
+
let messages = [...input.messages];
|
|
8466
|
+
let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
8467
|
+
let lastContent = null;
|
|
8468
|
+
for (let iteration = 0; iteration < HUB_CHAT_STEP_MAX_ITERATIONS; iteration += 1) {
|
|
8469
|
+
const result = await runCompletion(config, {
|
|
8470
|
+
model: input.model,
|
|
8471
|
+
messages,
|
|
8472
|
+
systemPrompt: input.systemPrompt,
|
|
8473
|
+
tools: mergedTools
|
|
8474
|
+
});
|
|
8475
|
+
usage = addUsage(usage, result.usage);
|
|
8476
|
+
lastContent = result.content;
|
|
8477
|
+
const toolCalls = result.toolCalls ?? [];
|
|
8478
|
+
if (toolCalls.length === 0) {
|
|
8479
|
+
return {
|
|
8480
|
+
content: result.content,
|
|
8481
|
+
usage
|
|
8482
|
+
};
|
|
8483
|
+
}
|
|
8484
|
+
const hubCalls = toolCalls.filter((call) => isHubMcpToolName(call.name));
|
|
8485
|
+
const passthroughCalls = toolCalls.filter((call) => !isHubMcpToolName(call.name));
|
|
8486
|
+
if (passthroughCalls.length > 0) {
|
|
8487
|
+
return {
|
|
8488
|
+
content: result.content,
|
|
8489
|
+
toolCalls: passthroughCalls,
|
|
8490
|
+
usage
|
|
8491
|
+
};
|
|
8492
|
+
}
|
|
8493
|
+
messages = [
|
|
8494
|
+
...messages,
|
|
8495
|
+
{
|
|
8496
|
+
role: "assistant",
|
|
8497
|
+
content: result.content,
|
|
8498
|
+
tool_calls: hubCalls
|
|
8499
|
+
}
|
|
8500
|
+
];
|
|
8501
|
+
for (const call of hubCalls) {
|
|
8502
|
+
const toolResult = await callTool(call.name, parseToolArguments(call.arguments));
|
|
8503
|
+
messages.push({
|
|
8504
|
+
role: "tool",
|
|
8505
|
+
tool_call_id: call.id,
|
|
8506
|
+
content: toolResult
|
|
8507
|
+
});
|
|
8508
|
+
}
|
|
8509
|
+
}
|
|
8510
|
+
return {
|
|
8511
|
+
content: lastContent,
|
|
8512
|
+
usage
|
|
8513
|
+
};
|
|
8514
|
+
}
|
|
8515
|
+
|
|
8110
8516
|
// src/server/routes/llm.ts
|
|
8111
8517
|
function sendMonthlyLimitExceeded(reply) {
|
|
8112
8518
|
return reply.code(402).send({
|
|
@@ -8224,7 +8630,7 @@ async function registerLlmRoutes(app, options) {
|
|
|
8224
8630
|
if (isNewTurn && isOverMonthlyLimit(totalTokens, user.llmMonthlyTokenLimit)) {
|
|
8225
8631
|
return sendMonthlyLimitExceeded(reply);
|
|
8226
8632
|
}
|
|
8227
|
-
const result = await
|
|
8633
|
+
const result = await runHubChatStep(llm, {
|
|
8228
8634
|
model,
|
|
8229
8635
|
messages,
|
|
8230
8636
|
tools,
|
|
@@ -8716,6 +9122,7 @@ function registerGracefulShutdown(app, ctx) {
|
|
|
8716
9122
|
const shutdown = async (signal) => {
|
|
8717
9123
|
app.log.info(`Received ${signal}, shutting down.`);
|
|
8718
9124
|
await app.close();
|
|
9125
|
+
await disposeHubMcpConnections();
|
|
8719
9126
|
await disconnectAll(ctx);
|
|
8720
9127
|
process.exit(0);
|
|
8721
9128
|
};
|