@exulu/backend 1.18.0 → 1.19.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/CHANGELOG.md +2 -3
- package/dist/index.cjs +495 -245
- package/dist/index.d.cts +39 -8
- package/dist/index.d.ts +39 -8
- package/dist/index.js +495 -245
- package/package.json +1 -1
- package/types/models/vector-methods.ts +2 -5
package/dist/index.cjs
CHANGED
|
@@ -531,6 +531,9 @@ var ExuluZodFileType = ({
|
|
|
531
531
|
});
|
|
532
532
|
};
|
|
533
533
|
var ExuluAgent = class {
|
|
534
|
+
// Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
|
|
535
|
+
// underscores and be a max length of 80 characters and at least 5 characters long.
|
|
536
|
+
// The ID is used for storing references to agents so it is important it does not change.
|
|
534
537
|
id;
|
|
535
538
|
name;
|
|
536
539
|
description = "";
|
|
@@ -938,38 +941,10 @@ var ExuluEval = class {
|
|
|
938
941
|
}
|
|
939
942
|
};
|
|
940
943
|
};
|
|
941
|
-
var calculateStatistics = (items, method) => {
|
|
942
|
-
let methodProperty = "";
|
|
943
|
-
if (method === "l1Distance") {
|
|
944
|
-
methodProperty = "l1_distance";
|
|
945
|
-
}
|
|
946
|
-
if (method === "l2Distance") {
|
|
947
|
-
methodProperty = "l2_distance";
|
|
948
|
-
}
|
|
949
|
-
if (method === "hammingDistance") {
|
|
950
|
-
methodProperty = "hamming_distance";
|
|
951
|
-
}
|
|
952
|
-
if (method === "jaccardDistance") {
|
|
953
|
-
methodProperty = "jaccard_distance";
|
|
954
|
-
}
|
|
955
|
-
if (method === "maxInnerProduct") {
|
|
956
|
-
methodProperty = "inner_product";
|
|
957
|
-
}
|
|
958
|
-
if (method === "cosineDistance") {
|
|
959
|
-
methodProperty = "cosine_distance";
|
|
960
|
-
}
|
|
961
|
-
const average = items.reduce((acc, item) => {
|
|
962
|
-
return acc + item[methodProperty];
|
|
963
|
-
}, 0) / items.length;
|
|
964
|
-
const total = items.reduce((acc, item) => {
|
|
965
|
-
return acc + item[methodProperty];
|
|
966
|
-
}, 0);
|
|
967
|
-
return {
|
|
968
|
-
average,
|
|
969
|
-
total
|
|
970
|
-
};
|
|
971
|
-
};
|
|
972
944
|
var ExuluTool = class {
|
|
945
|
+
// Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
|
|
946
|
+
// underscores and be a max length of 80 characters and at least 5 characters long.
|
|
947
|
+
// The ID is used for storing references to tools so it is important it does not change.
|
|
973
948
|
id;
|
|
974
949
|
name;
|
|
975
950
|
description;
|
|
@@ -991,13 +966,16 @@ var ExuluTool = class {
|
|
|
991
966
|
});
|
|
992
967
|
}
|
|
993
968
|
};
|
|
994
|
-
var getTableName = (
|
|
995
|
-
return sanitizeName(
|
|
969
|
+
var getTableName = (id) => {
|
|
970
|
+
return sanitizeName(id) + "_items";
|
|
996
971
|
};
|
|
997
|
-
var getChunksTableName = (
|
|
998
|
-
return sanitizeName(
|
|
972
|
+
var getChunksTableName = (id) => {
|
|
973
|
+
return sanitizeName(id) + "_chunks";
|
|
999
974
|
};
|
|
1000
975
|
var ExuluContext = class {
|
|
976
|
+
// Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
|
|
977
|
+
// underscores and be a max length of 80 characters and at least 5 characters long.
|
|
978
|
+
// The ID is used for the table name in the database, so it is important it does not change.
|
|
1001
979
|
id;
|
|
1002
980
|
name;
|
|
1003
981
|
active;
|
|
@@ -1014,7 +992,9 @@ var ExuluContext = class {
|
|
|
1014
992
|
this.name = name;
|
|
1015
993
|
this.fields = fields || [];
|
|
1016
994
|
this.configuration = configuration || {
|
|
1017
|
-
calculateVectors: "manual"
|
|
995
|
+
calculateVectors: "manual",
|
|
996
|
+
language: "english",
|
|
997
|
+
defaultRightsMode: "private"
|
|
1018
998
|
};
|
|
1019
999
|
this.description = description;
|
|
1020
1000
|
this.embedder = embedder;
|
|
@@ -1025,8 +1005,8 @@ var ExuluContext = class {
|
|
|
1025
1005
|
}
|
|
1026
1006
|
deleteAll = async () => {
|
|
1027
1007
|
const { db: db3 } = await postgresClient();
|
|
1028
|
-
await db3.from(getTableName(this.
|
|
1029
|
-
await db3.from(getChunksTableName(this.
|
|
1008
|
+
await db3.from(getTableName(this.id)).delete();
|
|
1009
|
+
await db3.from(getChunksTableName(this.id)).delete();
|
|
1030
1010
|
return {
|
|
1031
1011
|
count: 0,
|
|
1032
1012
|
results: []
|
|
@@ -1034,38 +1014,50 @@ var ExuluContext = class {
|
|
|
1034
1014
|
};
|
|
1035
1015
|
tableExists = async () => {
|
|
1036
1016
|
const { db: db3 } = await postgresClient();
|
|
1037
|
-
const
|
|
1017
|
+
const tableName = getTableName(this.id);
|
|
1018
|
+
console.log("[EXULU] checking if table exists.", tableName);
|
|
1019
|
+
const tableExists = await db3.schema.hasTable(tableName);
|
|
1038
1020
|
return tableExists;
|
|
1039
1021
|
};
|
|
1040
|
-
|
|
1022
|
+
chunksTableExists = async () => {
|
|
1023
|
+
const { db: db3 } = await postgresClient();
|
|
1024
|
+
const chunksTableName = getChunksTableName(this.id);
|
|
1025
|
+
const chunksTableExists = await db3.schema.hasTable(chunksTableName);
|
|
1026
|
+
return chunksTableExists;
|
|
1027
|
+
};
|
|
1028
|
+
createAndUpsertEmbeddings = async (item, user, statistics, role, job) => {
|
|
1041
1029
|
if (!this.embedder) {
|
|
1042
1030
|
throw new Error("Embedder is not set for this context.");
|
|
1043
1031
|
}
|
|
1032
|
+
if (!item.id) {
|
|
1033
|
+
throw new Error("Item id is required for generating embeddings.");
|
|
1034
|
+
}
|
|
1044
1035
|
const { db: db3 } = await postgresClient();
|
|
1045
1036
|
const { id: source, chunks } = await this.embedder.generateFromDocument({
|
|
1046
1037
|
...item,
|
|
1047
1038
|
id: item.id
|
|
1048
1039
|
}, {
|
|
1049
|
-
label: statistics
|
|
1050
|
-
trigger: statistics
|
|
1040
|
+
label: statistics?.label || this.name,
|
|
1041
|
+
trigger: statistics?.trigger || "agent"
|
|
1051
1042
|
}, user, role);
|
|
1052
|
-
const exists = await db3.schema.hasTable(getChunksTableName(this.
|
|
1043
|
+
const exists = await db3.schema.hasTable(getChunksTableName(this.id));
|
|
1053
1044
|
if (!exists) {
|
|
1054
1045
|
await this.createChunksTable();
|
|
1055
1046
|
}
|
|
1056
|
-
await db3.from(getChunksTableName(this.
|
|
1057
|
-
await db3.from(getChunksTableName(this.
|
|
1047
|
+
await db3.from(getChunksTableName(this.id)).where({ source }).delete();
|
|
1048
|
+
await db3.from(getChunksTableName(this.id)).insert(chunks.map((chunk) => ({
|
|
1058
1049
|
source,
|
|
1059
1050
|
content: chunk.content,
|
|
1060
1051
|
chunk_index: chunk.index,
|
|
1061
1052
|
embedding: import_knex4.default.toSql(chunk.vector)
|
|
1062
1053
|
})));
|
|
1063
|
-
await db3.from(getTableName(this.
|
|
1054
|
+
await db3.from(getTableName(this.id)).where({ id: item.id }).update({
|
|
1064
1055
|
embeddings_updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1065
1056
|
}).returning("id");
|
|
1066
1057
|
return {
|
|
1067
1058
|
id: item.id,
|
|
1068
|
-
chunks: chunks?.length || 0
|
|
1059
|
+
chunks: chunks?.length || 0,
|
|
1060
|
+
job
|
|
1069
1061
|
};
|
|
1070
1062
|
};
|
|
1071
1063
|
async updateItem(user, item, role, trigger) {
|
|
@@ -1091,43 +1083,105 @@ var ExuluContext = class {
|
|
|
1091
1083
|
delete payloadWithSanitizedPropertyNames.created_at;
|
|
1092
1084
|
delete payloadWithSanitizedPropertyNames.upsert;
|
|
1093
1085
|
payloadWithSanitizedPropertyNames.updated_at = db3.fn.now();
|
|
1094
|
-
const result = await db3.from(getTableName(this.
|
|
1086
|
+
const result = await db3.from(getTableName(this.id)).where({ id }).update(payloadWithSanitizedPropertyNames).returning("id");
|
|
1095
1087
|
if (this.embedder && (this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always")) {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
item: item.id,
|
|
1104
|
-
queue: this.embedder.queue,
|
|
1105
|
-
user,
|
|
1106
|
-
role,
|
|
1107
|
-
trigger: trigger || "agent"
|
|
1108
|
-
});
|
|
1109
|
-
return {
|
|
1110
|
-
id: result[0].id,
|
|
1111
|
-
job: job.id
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
await this.createAndUpsertEmbeddings(item, user, {
|
|
1115
|
-
label: this.embedder.name,
|
|
1088
|
+
const { job } = await this.embeddings.generate.one({
|
|
1089
|
+
item: {
|
|
1090
|
+
...item,
|
|
1091
|
+
id: result[0].id
|
|
1092
|
+
},
|
|
1093
|
+
user,
|
|
1094
|
+
role,
|
|
1116
1095
|
trigger: trigger || "agent"
|
|
1117
|
-
}
|
|
1096
|
+
});
|
|
1097
|
+
return {
|
|
1098
|
+
id: result[0].id,
|
|
1099
|
+
job
|
|
1100
|
+
};
|
|
1118
1101
|
}
|
|
1119
1102
|
return {
|
|
1120
1103
|
id: result[0].id,
|
|
1121
1104
|
job: void 0
|
|
1122
1105
|
};
|
|
1123
1106
|
}
|
|
1107
|
+
embeddings = {
|
|
1108
|
+
generate: {
|
|
1109
|
+
one: async ({
|
|
1110
|
+
item,
|
|
1111
|
+
user,
|
|
1112
|
+
role,
|
|
1113
|
+
trigger
|
|
1114
|
+
}) => {
|
|
1115
|
+
if (!this.embedder) {
|
|
1116
|
+
throw new Error("Embedder is not set for this context.");
|
|
1117
|
+
}
|
|
1118
|
+
if (!item.id) {
|
|
1119
|
+
throw new Error("Item id is required for generating embeddings.");
|
|
1120
|
+
}
|
|
1121
|
+
if (this.embedder.queue?.name) {
|
|
1122
|
+
console.log("[EXULU] embedder is in queue mode, scheduling job.");
|
|
1123
|
+
const job = await bullmqDecorator({
|
|
1124
|
+
label: `${this.embedder.name}`,
|
|
1125
|
+
embedder: this.embedder.id,
|
|
1126
|
+
context: this.id,
|
|
1127
|
+
inputs: item,
|
|
1128
|
+
item: item.id,
|
|
1129
|
+
queue: this.embedder.queue,
|
|
1130
|
+
user,
|
|
1131
|
+
role,
|
|
1132
|
+
trigger: trigger || "agent"
|
|
1133
|
+
});
|
|
1134
|
+
return {
|
|
1135
|
+
id: item.id,
|
|
1136
|
+
job: job.id,
|
|
1137
|
+
chunks: 0
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
return await this.createAndUpsertEmbeddings(item, user, {
|
|
1141
|
+
label: this.embedder.name,
|
|
1142
|
+
trigger: trigger || "agent"
|
|
1143
|
+
}, role, void 0);
|
|
1144
|
+
},
|
|
1145
|
+
all: async (context, userId, roleId) => {
|
|
1146
|
+
const exists = await context.tableExists();
|
|
1147
|
+
if (!exists) {
|
|
1148
|
+
await context.createItemsTable();
|
|
1149
|
+
}
|
|
1150
|
+
const chunksTableExists = await context.chunksTableExists();
|
|
1151
|
+
if (!chunksTableExists && context.embedder) {
|
|
1152
|
+
await context.createChunksTable();
|
|
1153
|
+
}
|
|
1154
|
+
const { db: db3 } = await postgresClient();
|
|
1155
|
+
const items = await db3.from(getTableName(context.id)).select("*");
|
|
1156
|
+
const jobs = [];
|
|
1157
|
+
if (!context.embedder?.queue?.name && items.length > 2e3) {
|
|
1158
|
+
throw new Error(`Embedder is not in queue mode, cannot generate embeddings for more than
|
|
1159
|
+
2.000 items at once, if you need to generate embeddings for more items please configure
|
|
1160
|
+
the embedder to use a queue. You can configure the embedder to use a queue by setting
|
|
1161
|
+
the queue property in the embedder configuration.`);
|
|
1162
|
+
}
|
|
1163
|
+
for (const item of items) {
|
|
1164
|
+
const { job } = await context.embeddings.generate.one({
|
|
1165
|
+
item,
|
|
1166
|
+
user: userId,
|
|
1167
|
+
role: roleId,
|
|
1168
|
+
trigger: "api"
|
|
1169
|
+
});
|
|
1170
|
+
if (job) {
|
|
1171
|
+
jobs.push(job);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return jobs || [];
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1124
1178
|
async insertItem(user, item, upsert = false, role, trigger) {
|
|
1125
1179
|
if (!item.name) {
|
|
1126
1180
|
throw new Error("Name field is required.");
|
|
1127
1181
|
}
|
|
1128
1182
|
const { db: db3 } = await postgresClient();
|
|
1129
1183
|
if (item.external_id) {
|
|
1130
|
-
const existingItem = await db3.from(getTableName(this.
|
|
1184
|
+
const existingItem = await db3.from(getTableName(this.id)).where({ external_id: item.external_id }).first();
|
|
1131
1185
|
if (existingItem && !upsert) {
|
|
1132
1186
|
throw new Error("Item with external id " + item.external_id + " already exists.");
|
|
1133
1187
|
}
|
|
@@ -1140,7 +1194,7 @@ var ExuluContext = class {
|
|
|
1140
1194
|
}
|
|
1141
1195
|
}
|
|
1142
1196
|
if (upsert && item.id) {
|
|
1143
|
-
const existingItem = await db3.from(getTableName(this.
|
|
1197
|
+
const existingItem = await db3.from(getTableName(this.id)).where({ id: item.id }).first();
|
|
1144
1198
|
if (existingItem && upsert) {
|
|
1145
1199
|
await this.updateItem(user, {
|
|
1146
1200
|
...item,
|
|
@@ -1164,39 +1218,26 @@ var ExuluContext = class {
|
|
|
1164
1218
|
});
|
|
1165
1219
|
delete payloadWithSanitizedPropertyNames.id;
|
|
1166
1220
|
delete payloadWithSanitizedPropertyNames.upsert;
|
|
1167
|
-
const result = await db3.from(getTableName(this.
|
|
1221
|
+
const result = await db3.from(getTableName(this.id)).insert({
|
|
1168
1222
|
...payloadWithSanitizedPropertyNames,
|
|
1169
1223
|
id: db3.fn.uuid(),
|
|
1170
1224
|
created_at: db3.fn.now(),
|
|
1171
1225
|
updated_at: db3.fn.now()
|
|
1172
1226
|
}).returning("id");
|
|
1173
1227
|
if (this.embedder && (this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always")) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
item: item.id,
|
|
1182
|
-
queue: this.embedder.queue,
|
|
1183
|
-
user,
|
|
1184
|
-
role,
|
|
1185
|
-
trigger: trigger || "agent"
|
|
1186
|
-
});
|
|
1187
|
-
return {
|
|
1188
|
-
id: result[0].id,
|
|
1189
|
-
job: job.id
|
|
1190
|
-
};
|
|
1191
|
-
}
|
|
1192
|
-
console.log("[EXULU] embedder is not in queue mode, calculating vectors directly.");
|
|
1193
|
-
await this.createAndUpsertEmbeddings({
|
|
1194
|
-
...item,
|
|
1195
|
-
id: result[0].id
|
|
1196
|
-
}, user, {
|
|
1197
|
-
label: this.embedder.name,
|
|
1228
|
+
const { job } = await this.embeddings.generate.one({
|
|
1229
|
+
item: {
|
|
1230
|
+
...item,
|
|
1231
|
+
id: result[0].id
|
|
1232
|
+
},
|
|
1233
|
+
user,
|
|
1234
|
+
role,
|
|
1198
1235
|
trigger: trigger || "agent"
|
|
1199
|
-
}
|
|
1236
|
+
});
|
|
1237
|
+
return {
|
|
1238
|
+
id: result[0].id,
|
|
1239
|
+
job
|
|
1240
|
+
};
|
|
1200
1241
|
}
|
|
1201
1242
|
return {
|
|
1202
1243
|
id: result[0].id,
|
|
@@ -1225,7 +1266,7 @@ var ExuluContext = class {
|
|
|
1225
1266
|
if (page < 1) page = 1;
|
|
1226
1267
|
if (limit < 1) limit = 10;
|
|
1227
1268
|
let offset = (page - 1) * limit;
|
|
1228
|
-
const mainTable = getTableName(this.
|
|
1269
|
+
const mainTable = getTableName(this.id);
|
|
1229
1270
|
const { db: db3 } = await postgresClient();
|
|
1230
1271
|
const columns = await db3(mainTable).columnInfo();
|
|
1231
1272
|
const totalQuery = db3.count("* as count").from(mainTable).first();
|
|
@@ -1288,7 +1329,7 @@ var ExuluContext = class {
|
|
|
1288
1329
|
if (this.queryRewriter) {
|
|
1289
1330
|
query = await this.queryRewriter(query);
|
|
1290
1331
|
}
|
|
1291
|
-
const chunksTable = getChunksTableName(this.
|
|
1332
|
+
const chunksTable = getChunksTableName(this.id);
|
|
1292
1333
|
itemsQuery.leftJoin(chunksTable, function() {
|
|
1293
1334
|
this.on(chunksTable + ".source", "=", mainTable + ".id");
|
|
1294
1335
|
});
|
|
@@ -1308,52 +1349,133 @@ var ExuluContext = class {
|
|
|
1308
1349
|
const vector = chunks[0].vector;
|
|
1309
1350
|
const vectorStr = `ARRAY[${vector.join(",")}]`;
|
|
1310
1351
|
const vectorExpr = `${vectorStr}::vector`;
|
|
1352
|
+
const language = this.configuration.language || "english";
|
|
1353
|
+
let items = [];
|
|
1311
1354
|
switch (method) {
|
|
1312
|
-
case "
|
|
1313
|
-
itemsQuery.select(db3.raw(
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
itemsQuery.select(db3.raw(`?? <#> ${vectorExpr} as hamming_distance`, [`${chunksTable}.embedding`]));
|
|
1322
|
-
itemsQuery.orderByRaw(db3.raw(`?? <#> ${vectorExpr} ASC`, [`${chunksTable}.embedding`]));
|
|
1323
|
-
break;
|
|
1324
|
-
case "jaccardDistance":
|
|
1325
|
-
itemsQuery.select(db3.raw(`?? <#> ${vectorExpr} as jaccard_distance`, [`${chunksTable}.embedding`]));
|
|
1326
|
-
itemsQuery.orderByRaw(db3.raw(`?? <#> ${vectorExpr} ASC`, [`${chunksTable}.embedding`]));
|
|
1327
|
-
break;
|
|
1328
|
-
case "maxInnerProduct":
|
|
1329
|
-
itemsQuery.select(db3.raw(`?? <#> ${vectorExpr} as inner_product`, [`${chunksTable}.embedding`]));
|
|
1330
|
-
itemsQuery.orderByRaw(db3.raw(`?? <#> ${vectorExpr} ASC`, [`${chunksTable}.embedding`]));
|
|
1355
|
+
case "tsvector":
|
|
1356
|
+
itemsQuery.select(db3.raw(
|
|
1357
|
+
`ts_rank(${chunksTable}.fts, websearch_to_tsquery(?, ?)) as fts_rank`,
|
|
1358
|
+
[language, query]
|
|
1359
|
+
)).whereRaw(
|
|
1360
|
+
`${chunksTable}.fts @@ websearch_to_tsquery(?, ?)`,
|
|
1361
|
+
[language, query]
|
|
1362
|
+
).orderByRaw(`fts_rank DESC`);
|
|
1363
|
+
items = await itemsQuery;
|
|
1331
1364
|
break;
|
|
1332
1365
|
case "cosineDistance":
|
|
1333
1366
|
default:
|
|
1334
|
-
itemsQuery.
|
|
1335
|
-
itemsQuery.
|
|
1367
|
+
itemsQuery.whereNotNull(`${chunksTable}.embedding`);
|
|
1368
|
+
itemsQuery.select(
|
|
1369
|
+
db3.raw(`1 - (${chunksTable}.embedding <=> ${vectorExpr}) AS cosine_distance`)
|
|
1370
|
+
);
|
|
1371
|
+
itemsQuery.orderByRaw(
|
|
1372
|
+
`${chunksTable}.embedding <=> ${vectorExpr} ASC NULLS LAST`
|
|
1373
|
+
);
|
|
1374
|
+
items = await itemsQuery;
|
|
1336
1375
|
break;
|
|
1376
|
+
case "hybridSearch":
|
|
1377
|
+
const matchCount = Math.min(limit * 5, 30);
|
|
1378
|
+
const fullTextWeight = 1;
|
|
1379
|
+
const semanticWeight = 1;
|
|
1380
|
+
const rrfK = 50;
|
|
1381
|
+
const hybridSQL = `
|
|
1382
|
+
WITH full_text AS (
|
|
1383
|
+
SELECT
|
|
1384
|
+
c.id,
|
|
1385
|
+
c.source,
|
|
1386
|
+
row_number() OVER (
|
|
1387
|
+
ORDER BY ts_rank_cd(c.fts, websearch_to_tsquery(?, ?)) DESC
|
|
1388
|
+
) AS rank_ix
|
|
1389
|
+
FROM ${chunksTable} c
|
|
1390
|
+
WHERE c.fts @@ websearch_to_tsquery(?, ?)
|
|
1391
|
+
ORDER BY rank_ix
|
|
1392
|
+
LIMIT LEAST(?, 30) * 2
|
|
1393
|
+
),
|
|
1394
|
+
semantic AS (
|
|
1395
|
+
SELECT
|
|
1396
|
+
c.id,
|
|
1397
|
+
c.source,
|
|
1398
|
+
row_number() OVER (
|
|
1399
|
+
ORDER BY c.embedding <=> ${vectorExpr} ASC
|
|
1400
|
+
) AS rank_ix
|
|
1401
|
+
FROM ${chunksTable} c
|
|
1402
|
+
WHERE c.embedding IS NOT NULL
|
|
1403
|
+
ORDER BY rank_ix
|
|
1404
|
+
LIMIT LEAST(?, 30) * 2
|
|
1405
|
+
)
|
|
1406
|
+
SELECT
|
|
1407
|
+
m.*,
|
|
1408
|
+
c.id AS chunk_id,
|
|
1409
|
+
c.source,
|
|
1410
|
+
c.content,
|
|
1411
|
+
c.chunk_index,
|
|
1412
|
+
c.created_at AS chunk_created_at,
|
|
1413
|
+
c.updated_at AS chunk_updated_at,
|
|
1414
|
+
|
|
1415
|
+
/* Per-signal scores for introspection */
|
|
1416
|
+
ts_rank(c.fts, websearch_to_tsquery(?, ?)) AS fts_rank,
|
|
1417
|
+
(1 - (c.embedding <=> ${vectorExpr})) AS cosine_distance,
|
|
1418
|
+
|
|
1419
|
+
/* Hybrid RRF score */
|
|
1420
|
+
(
|
|
1421
|
+
COALESCE(1.0 / (? + ft.rank_ix), 0.0) * ?
|
|
1422
|
+
+
|
|
1423
|
+
COALESCE(1.0 / (? + se.rank_ix), 0.0) * ?
|
|
1424
|
+
) AS hybrid_score
|
|
1425
|
+
|
|
1426
|
+
FROM full_text ft
|
|
1427
|
+
FULL OUTER JOIN semantic se
|
|
1428
|
+
ON ft.id = se.id
|
|
1429
|
+
JOIN ${chunksTable} c
|
|
1430
|
+
ON COALESCE(ft.id, se.id) = c.id
|
|
1431
|
+
JOIN ${mainTable} m
|
|
1432
|
+
ON m.id = c.source
|
|
1433
|
+
ORDER BY hybrid_score DESC
|
|
1434
|
+
LIMIT LEAST(?, 30)
|
|
1435
|
+
OFFSET 0
|
|
1436
|
+
`;
|
|
1437
|
+
const bindings = [
|
|
1438
|
+
// full_text: websearch_to_tsquery(lang, query) in rank and where
|
|
1439
|
+
language,
|
|
1440
|
+
query,
|
|
1441
|
+
language,
|
|
1442
|
+
query,
|
|
1443
|
+
matchCount,
|
|
1444
|
+
// full_text limit
|
|
1445
|
+
matchCount,
|
|
1446
|
+
// semantic limit
|
|
1447
|
+
// fts_rank (ts_rank) call
|
|
1448
|
+
language,
|
|
1449
|
+
query,
|
|
1450
|
+
// RRF fusion parameters
|
|
1451
|
+
rrfK,
|
|
1452
|
+
fullTextWeight,
|
|
1453
|
+
rrfK,
|
|
1454
|
+
semanticWeight,
|
|
1455
|
+
matchCount
|
|
1456
|
+
// final limit
|
|
1457
|
+
];
|
|
1458
|
+
items = await db3.raw(hybridSQL, bindings).then((r) => r.rows ?? r);
|
|
1337
1459
|
}
|
|
1338
|
-
|
|
1460
|
+
console.log("items", items.map((item) => item.name + " " + item.id));
|
|
1339
1461
|
const seenSources = /* @__PURE__ */ new Map();
|
|
1340
1462
|
items = items.reduce((acc, item) => {
|
|
1341
1463
|
if (!seenSources.has(item.source)) {
|
|
1342
1464
|
seenSources.set(item.source, {
|
|
1343
1465
|
...Object.fromEntries(
|
|
1344
1466
|
Object.keys(item).filter(
|
|
1345
|
-
(key) => key !== "
|
|
1467
|
+
(key) => key !== "cosine_distance" && // kept per chunk below
|
|
1468
|
+
key !== "fts_rank" && // kept per chunk below
|
|
1469
|
+
key !== "hybrid_score" && // we will compute per item below
|
|
1470
|
+
key !== "content" && key !== "source" && key !== "chunk_index" && key !== "chunk_id" && key !== "chunk_created_at" && key !== "chunk_updated_at"
|
|
1346
1471
|
).map((key) => [key, item[key]])
|
|
1347
1472
|
),
|
|
1348
1473
|
chunks: [{
|
|
1349
1474
|
content: item.content,
|
|
1350
1475
|
chunk_index: item.chunk_index,
|
|
1351
|
-
...method === "
|
|
1352
|
-
...method === "
|
|
1353
|
-
...method === "
|
|
1354
|
-
...method === "jaccardDistance" && { jaccard_distance: item.jaccard_distance },
|
|
1355
|
-
...method === "maxInnerProduct" && { inner_product: item.inner_product },
|
|
1356
|
-
...method === "cosineDistance" && { cosine_distance: item.cosine_distance }
|
|
1476
|
+
...method === "cosineDistance" && { cosine_distance: item.cosine_distance },
|
|
1477
|
+
...(method === "tsvector" || method === "hybridSearch") && { fts_rank: item.fts_rank },
|
|
1478
|
+
...method === "hybridSearch" && { hybrid_score: item.hybrid_score }
|
|
1357
1479
|
}]
|
|
1358
1480
|
});
|
|
1359
1481
|
acc.push(seenSources.get(item.source));
|
|
@@ -1361,12 +1483,9 @@ var ExuluContext = class {
|
|
|
1361
1483
|
seenSources.get(item.source).chunks.push({
|
|
1362
1484
|
content: item.content,
|
|
1363
1485
|
chunk_index: item.chunk_index,
|
|
1364
|
-
...method === "
|
|
1365
|
-
...method === "
|
|
1366
|
-
...method === "
|
|
1367
|
-
...method === "jaccardDistance" && { jaccard_distance: item.jaccard_distance },
|
|
1368
|
-
...method === "maxInnerProduct" && { inner_product: item.inner_product },
|
|
1369
|
-
...method === "cosineDistance" && { cosine_distance: item.cosine_distance }
|
|
1486
|
+
...method === "cosineDistance" && { cosine_distance: item.cosine_distance },
|
|
1487
|
+
...(method === "tsvector" || method === "hybridSearch") && { fts_rank: item.fts_rank },
|
|
1488
|
+
...method === "hybridSearch" && { hybrid_score: item.hybrid_score }
|
|
1370
1489
|
});
|
|
1371
1490
|
}
|
|
1372
1491
|
return acc;
|
|
@@ -1375,16 +1494,34 @@ var ExuluContext = class {
|
|
|
1375
1494
|
if (!item.chunks?.length) {
|
|
1376
1495
|
return;
|
|
1377
1496
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
total
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1497
|
+
if (method === "tsvector") {
|
|
1498
|
+
const ranks = item.chunks.map((c) => typeof c.fts_rank === "number" ? c.fts_rank : 0);
|
|
1499
|
+
const total = ranks.reduce((a, b) => a + b, 0);
|
|
1500
|
+
const average = ranks.length ? total / ranks.length : 0;
|
|
1501
|
+
item.averageRelevance = average;
|
|
1502
|
+
item.totalRelevance = total;
|
|
1503
|
+
} else if (method === "cosineDistance") {
|
|
1504
|
+
let methodProperty = "cosine_distance";
|
|
1505
|
+
const average = item.chunks.reduce((acc, item2) => {
|
|
1506
|
+
return acc + item2[methodProperty];
|
|
1507
|
+
}, 0) / item.chunks.length;
|
|
1508
|
+
const total = item.chunks.reduce((acc, item2) => {
|
|
1509
|
+
return acc + item2[methodProperty];
|
|
1510
|
+
}, 0);
|
|
1511
|
+
item.averageRelevance = average;
|
|
1512
|
+
item.totalRelevance = total;
|
|
1513
|
+
} else if (method === "hybridSearch") {
|
|
1514
|
+
const scores = item.chunks.map((c) => typeof c.hybrid_score === "number" ? c.hybrid_score : 0);
|
|
1515
|
+
const total = scores.reduce((a, b) => a + b, 0);
|
|
1516
|
+
const average = scores.length ? total / scores.length : 0;
|
|
1517
|
+
item.averageRelevance = average;
|
|
1518
|
+
item.totalRelevance = total;
|
|
1519
|
+
}
|
|
1384
1520
|
});
|
|
1385
1521
|
if (this.resultReranker && query) {
|
|
1386
1522
|
items = await this.resultReranker(items);
|
|
1387
1523
|
}
|
|
1524
|
+
items = items.slice(0, limit);
|
|
1388
1525
|
return {
|
|
1389
1526
|
filters: {
|
|
1390
1527
|
archived,
|
|
@@ -1402,7 +1539,7 @@ var ExuluContext = class {
|
|
|
1402
1539
|
};
|
|
1403
1540
|
createItemsTable = async () => {
|
|
1404
1541
|
const { db: db3 } = await postgresClient();
|
|
1405
|
-
const tableName = getTableName(this.
|
|
1542
|
+
const tableName = getTableName(this.id);
|
|
1406
1543
|
console.log("[EXULU] Creating table: " + tableName);
|
|
1407
1544
|
return await db3.schema.createTable(tableName, (table) => {
|
|
1408
1545
|
console.log("[EXULU] Creating fields for table.", this.fields);
|
|
@@ -1428,19 +1565,33 @@ var ExuluContext = class {
|
|
|
1428
1565
|
};
|
|
1429
1566
|
createChunksTable = async () => {
|
|
1430
1567
|
const { db: db3 } = await postgresClient();
|
|
1431
|
-
const tableName = getChunksTableName(this.
|
|
1568
|
+
const tableName = getChunksTableName(this.id);
|
|
1432
1569
|
console.log("[EXULU] Creating table: " + tableName);
|
|
1433
|
-
|
|
1570
|
+
await db3.schema.createTable(tableName, (table) => {
|
|
1434
1571
|
if (!this.embedder) {
|
|
1435
1572
|
throw new Error("Embedder must be set for context " + this.name + " to create chunks table.");
|
|
1436
1573
|
}
|
|
1437
1574
|
table.uuid("id").primary().defaultTo(db3.fn.uuid());
|
|
1438
|
-
table.uuid("source").references("id").inTable(getTableName(this.
|
|
1575
|
+
table.uuid("source").references("id").inTable(getTableName(this.id));
|
|
1439
1576
|
table.text("content");
|
|
1440
1577
|
table.integer("chunk_index");
|
|
1441
1578
|
table.specificType("embedding", `vector(${this.embedder.vectorDimensions})`);
|
|
1579
|
+
table.specificType(
|
|
1580
|
+
"fts",
|
|
1581
|
+
`tsvector GENERATED ALWAYS AS (to_tsvector('${this.configuration.language || "english"}', coalesce(content, ''))) STORED`
|
|
1582
|
+
);
|
|
1583
|
+
table.index(["fts"], `${tableName}_fts_gin_idx`, "gin");
|
|
1584
|
+
table.index(["source"], `${tableName}_source_idx`);
|
|
1442
1585
|
table.timestamps(true, true);
|
|
1443
1586
|
});
|
|
1587
|
+
await db3.raw(`
|
|
1588
|
+
CREATE INDEX IF NOT EXISTS ${tableName}_embedding_hnsw_cosine
|
|
1589
|
+
ON ${tableName}
|
|
1590
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
1591
|
+
WHERE embedding IS NOT NULL
|
|
1592
|
+
WITH (m = 16, ef_construction = 64)
|
|
1593
|
+
`);
|
|
1594
|
+
return;
|
|
1444
1595
|
};
|
|
1445
1596
|
// Exports the context as a tool that can be used by an agent
|
|
1446
1597
|
tool = () => {
|
|
@@ -1858,9 +2009,6 @@ var requestValidators = {
|
|
|
1858
2009
|
}
|
|
1859
2010
|
};
|
|
1860
2011
|
|
|
1861
|
-
// src/registry/routes.ts
|
|
1862
|
-
var import_zodex = require("zodex");
|
|
1863
|
-
|
|
1864
2012
|
// src/bullmq/queues.ts
|
|
1865
2013
|
var import_bullmq4 = require("bullmq");
|
|
1866
2014
|
var import_bullmq_otel = require("bullmq-otel");
|
|
@@ -1897,17 +2045,13 @@ var queues = new ExuluQueues();
|
|
|
1897
2045
|
// types/models/vector-methods.ts
|
|
1898
2046
|
var VectorMethodEnum = {
|
|
1899
2047
|
"cosineDistance": "cosineDistance",
|
|
1900
|
-
"
|
|
1901
|
-
"
|
|
1902
|
-
"hammingDistance": "hammingDistance",
|
|
1903
|
-
"jaccardDistance": "jaccardDistance",
|
|
1904
|
-
"maxInnerProduct": "maxInnerProduct"
|
|
2048
|
+
"hybridSearch": "hybridSearch",
|
|
2049
|
+
"tsvector": "tsvector"
|
|
1905
2050
|
};
|
|
1906
2051
|
|
|
1907
2052
|
// src/registry/routes.ts
|
|
1908
2053
|
var import_express4 = __toESM(require("express"), 1);
|
|
1909
2054
|
var import_server3 = require("@apollo/server");
|
|
1910
|
-
var Papa = require("papaparse");
|
|
1911
2055
|
var import_cors = __toESM(require("cors"), 1);
|
|
1912
2056
|
var import_reflect_metadata = require("reflect-metadata");
|
|
1913
2057
|
|
|
@@ -3149,7 +3293,7 @@ function createSDL(tables, contexts, agents, tools) {
|
|
|
3149
3293
|
const contextSchemas = [];
|
|
3150
3294
|
console.log("============= Agents =============", agents?.length);
|
|
3151
3295
|
contexts.forEach((context) => {
|
|
3152
|
-
const tableName = getTableName(context.
|
|
3296
|
+
const tableName = getTableName(context.id);
|
|
3153
3297
|
const definition = {
|
|
3154
3298
|
name: {
|
|
3155
3299
|
singular: tableName,
|
|
@@ -3476,30 +3620,6 @@ type StatisticsResult {
|
|
|
3476
3620
|
typeDefs: fullSDL,
|
|
3477
3621
|
resolvers
|
|
3478
3622
|
});
|
|
3479
|
-
console.log("\n\u{1F4CA} GraphQL Schema Overview\n");
|
|
3480
|
-
const queriesTable = Object.keys(resolvers.Query).map((query) => ({
|
|
3481
|
-
"Operation Type": "Query",
|
|
3482
|
-
"Name": query,
|
|
3483
|
-
"Description": "Retrieves data"
|
|
3484
|
-
}));
|
|
3485
|
-
const mutationsTable = Object.keys(resolvers.Mutation).map((mutation) => ({
|
|
3486
|
-
"Operation Type": "Mutation",
|
|
3487
|
-
"Name": mutation,
|
|
3488
|
-
"Description": "Modifies data"
|
|
3489
|
-
}));
|
|
3490
|
-
const typesTable = tables.flatMap(
|
|
3491
|
-
(table) => table.fields.map((field) => ({
|
|
3492
|
-
"Type": table.name.singular,
|
|
3493
|
-
"Field": field.name,
|
|
3494
|
-
"Field Type": field.type,
|
|
3495
|
-
"Required": field.required ? "Yes" : "No"
|
|
3496
|
-
}))
|
|
3497
|
-
);
|
|
3498
|
-
console.log("\u{1F50D} Operations:");
|
|
3499
|
-
console.table([...queriesTable, ...mutationsTable]);
|
|
3500
|
-
console.log("\n\u{1F4DD} Types and Fields:");
|
|
3501
|
-
console.table(typesTable);
|
|
3502
|
-
console.log("\n");
|
|
3503
3623
|
return schema;
|
|
3504
3624
|
}
|
|
3505
3625
|
var encryptSensitiveFields = (input) => {
|
|
@@ -4001,12 +4121,9 @@ var createRecurringJobs = async () => {
|
|
|
4001
4121
|
}
|
|
4002
4122
|
}
|
|
4003
4123
|
);
|
|
4004
|
-
console.log("[EXULU] recurring job schedulers:");
|
|
4005
|
-
console.table(recurringJobSchedulersLogs);
|
|
4006
4124
|
return queue;
|
|
4007
4125
|
};
|
|
4008
4126
|
var createExpressRoutes = async (app, logger, agents, tools, contexts, config, tracer) => {
|
|
4009
|
-
const routeLogs = [];
|
|
4010
4127
|
console.log("============= agents =============", agents?.length);
|
|
4011
4128
|
var corsOptions = {
|
|
4012
4129
|
origin: "*",
|
|
@@ -4029,35 +4146,6 @@ var createExpressRoutes = async (app, logger, agents, tools, contexts, config, t
|
|
|
4029
4146
|
Intelligence Management Platform
|
|
4030
4147
|
|
|
4031
4148
|
`);
|
|
4032
|
-
console.log("Agents:");
|
|
4033
|
-
console.table(agents.map((agent) => {
|
|
4034
|
-
return {
|
|
4035
|
-
id: agent.id,
|
|
4036
|
-
name: agent.name,
|
|
4037
|
-
description: agent.description,
|
|
4038
|
-
slug: "/agents/" + agent.id,
|
|
4039
|
-
active: true
|
|
4040
|
-
};
|
|
4041
|
-
}));
|
|
4042
|
-
console.log("Contexts:");
|
|
4043
|
-
console.table(contexts.map((context) => {
|
|
4044
|
-
return {
|
|
4045
|
-
id: context.id,
|
|
4046
|
-
name: context.name,
|
|
4047
|
-
description: context.description,
|
|
4048
|
-
embedder: context.embedder?.name || void 0,
|
|
4049
|
-
slug: "/contexts/" + context.id,
|
|
4050
|
-
active: context.active
|
|
4051
|
-
};
|
|
4052
|
-
}));
|
|
4053
|
-
routeLogs.push(
|
|
4054
|
-
{ route: "/contexts", method: "GET", note: "List all contexts" },
|
|
4055
|
-
{ route: "/contexts/:id", method: "GET", note: "Get specific context" },
|
|
4056
|
-
{ route: "/items/:context", method: "POST", note: "Create new item in context" },
|
|
4057
|
-
{ route: "/items/:context", method: "GET", note: "Get items from context" },
|
|
4058
|
-
{ route: "/items/export/:context", method: "GET", note: "Export items from context" },
|
|
4059
|
-
{ route: "/graphql", method: "POST", note: "GraphQL endpoint" }
|
|
4060
|
-
);
|
|
4061
4149
|
if (redisServer.host?.length && redisServer.port?.length) {
|
|
4062
4150
|
await createRecurringJobs();
|
|
4063
4151
|
} else {
|
|
@@ -4076,15 +4164,12 @@ var createExpressRoutes = async (app, logger, agents, tools, contexts, config, t
|
|
|
4076
4164
|
statisticsSchema2(),
|
|
4077
4165
|
rbacSchema2()
|
|
4078
4166
|
], contexts, agents, tools);
|
|
4079
|
-
console.log("[EXULU] graphql server");
|
|
4080
4167
|
const server = new import_server3.ApolloServer({
|
|
4081
4168
|
cache: new import_utils2.InMemoryLRUCache(),
|
|
4082
4169
|
schema,
|
|
4083
4170
|
introspection: true
|
|
4084
4171
|
});
|
|
4085
|
-
console.log("[EXULU] starting graphql server");
|
|
4086
4172
|
await server.start();
|
|
4087
|
-
console.log("[EXULU] graphql server started");
|
|
4088
4173
|
app.use(
|
|
4089
4174
|
"/graphql",
|
|
4090
4175
|
(0, import_cors.default)(corsOptions),
|
|
@@ -4223,9 +4308,9 @@ Mood: friendly and intelligent.
|
|
|
4223
4308
|
}
|
|
4224
4309
|
const exists = await context.tableExists();
|
|
4225
4310
|
if (!exists) {
|
|
4226
|
-
throw new Error("Table with name " + getTableName(context.
|
|
4311
|
+
throw new Error("Table with name " + getTableName(context.id) + " does not exist.");
|
|
4227
4312
|
}
|
|
4228
|
-
const query = db3.from(getTableName(context.
|
|
4313
|
+
const query = db3.from(getTableName(context.id)).select("id");
|
|
4229
4314
|
if (id) {
|
|
4230
4315
|
query.where({ id });
|
|
4231
4316
|
}
|
|
@@ -4237,12 +4322,12 @@ Mood: friendly and intelligent.
|
|
|
4237
4322
|
return null;
|
|
4238
4323
|
}
|
|
4239
4324
|
if (context.embedder) {
|
|
4240
|
-
const chunks = await db3.from(getChunksTableName(context.
|
|
4325
|
+
const chunks = await db3.from(getChunksTableName(context.id)).where({ source: item.id }).select("id");
|
|
4241
4326
|
if (chunks.length > 0) {
|
|
4242
|
-
await db3.from(getChunksTableName(context.
|
|
4327
|
+
await db3.from(getChunksTableName(context.id)).where({ source: item.id }).delete();
|
|
4243
4328
|
}
|
|
4244
4329
|
}
|
|
4245
|
-
const mutation = db3.from(getTableName(context.
|
|
4330
|
+
const mutation = db3.from(getTableName(context.id)).where({ id: item.id }).delete().returning("id");
|
|
4246
4331
|
const result = await mutation;
|
|
4247
4332
|
return result;
|
|
4248
4333
|
};
|
|
@@ -4284,6 +4369,47 @@ Mood: friendly and intelligent.
|
|
|
4284
4369
|
}
|
|
4285
4370
|
res.status(200).json(result);
|
|
4286
4371
|
});
|
|
4372
|
+
app.delete("/items/:context/:id/chunks/delete", async (req, res) => {
|
|
4373
|
+
console.log("[EXULU] deleting chunks for item.", req.params.id);
|
|
4374
|
+
const authenticationResult = await requestValidators.authenticate(req);
|
|
4375
|
+
if (!authenticationResult.user?.id) {
|
|
4376
|
+
res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
|
|
4377
|
+
return;
|
|
4378
|
+
}
|
|
4379
|
+
const { db: db3 } = await postgresClient();
|
|
4380
|
+
const context = contexts.find((context2) => context2.id === req.params.context);
|
|
4381
|
+
if (!context) {
|
|
4382
|
+
res.status(400).json({
|
|
4383
|
+
message: "Context not found in registry."
|
|
4384
|
+
});
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
await db3.from(getChunksTableName(context.id)).where({ source: req.params.id }).delete();
|
|
4388
|
+
res.status(200).json({
|
|
4389
|
+
message: "Chunks deleted successfully."
|
|
4390
|
+
});
|
|
4391
|
+
});
|
|
4392
|
+
app.delete("/items/:context/chunks/delete", async (req, res) => {
|
|
4393
|
+
console.log("[EXULU] deleting chunks for context.", req.params.context);
|
|
4394
|
+
const authenticationResult = await requestValidators.authenticate(req);
|
|
4395
|
+
if (!authenticationResult.user?.id) {
|
|
4396
|
+
res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
|
|
4397
|
+
return;
|
|
4398
|
+
}
|
|
4399
|
+
const { db: db3 } = await postgresClient();
|
|
4400
|
+
const context = contexts.find((context2) => context2.id === req.params.context);
|
|
4401
|
+
if (!context) {
|
|
4402
|
+
res.status(400).json({
|
|
4403
|
+
message: "Context not found in registry."
|
|
4404
|
+
});
|
|
4405
|
+
return;
|
|
4406
|
+
}
|
|
4407
|
+
console.log("[EXULU] truncating chunks table for context.", context.id);
|
|
4408
|
+
await db3.from(getChunksTableName(context.id)).truncate();
|
|
4409
|
+
res.status(200).json({
|
|
4410
|
+
message: "Chunks deleted successfully."
|
|
4411
|
+
});
|
|
4412
|
+
});
|
|
4287
4413
|
app.get("/items/:context/:id", async (req, res) => {
|
|
4288
4414
|
if (!req.params.context) {
|
|
4289
4415
|
res.status(400).json({
|
|
@@ -4306,14 +4432,15 @@ Mood: friendly and intelligent.
|
|
|
4306
4432
|
return;
|
|
4307
4433
|
}
|
|
4308
4434
|
const itemsTableExists = await context.tableExists();
|
|
4435
|
+
console.log("[EXULU] items table exists.", itemsTableExists);
|
|
4309
4436
|
if (!itemsTableExists) {
|
|
4310
4437
|
await context.createItemsTable();
|
|
4311
4438
|
}
|
|
4312
|
-
const chunksTableExists = await
|
|
4439
|
+
const chunksTableExists = await context.chunksTableExists();
|
|
4313
4440
|
if (!chunksTableExists && context.embedder) {
|
|
4314
4441
|
await context.createChunksTable();
|
|
4315
4442
|
}
|
|
4316
|
-
const item = await db3.from(getTableName(context.
|
|
4443
|
+
const item = await db3.from(getTableName(context.id)).where({ id: req.params.id }).select("*").first();
|
|
4317
4444
|
if (!item) {
|
|
4318
4445
|
res.status(404).json({
|
|
4319
4446
|
message: "Item not found."
|
|
@@ -4326,8 +4453,8 @@ Mood: friendly and intelligent.
|
|
|
4326
4453
|
});
|
|
4327
4454
|
return;
|
|
4328
4455
|
}
|
|
4329
|
-
console.log("[EXULU] chunks table name.", getChunksTableName(context.
|
|
4330
|
-
const chunks = await db3.from(getChunksTableName(context.
|
|
4456
|
+
console.log("[EXULU] chunks table name.", getChunksTableName(context.id));
|
|
4457
|
+
const chunks = await db3.from(getChunksTableName(context.id)).where({ source: req.params.id }).select("id", "content", "source", "embedding", "chunk_index", "created_at", "updated_at");
|
|
4331
4458
|
console.log("[EXULU] chunks", chunks);
|
|
4332
4459
|
res.status(200).json({
|
|
4333
4460
|
...item,
|
|
@@ -4342,6 +4469,88 @@ Mood: friendly and intelligent.
|
|
|
4342
4469
|
}))
|
|
4343
4470
|
});
|
|
4344
4471
|
});
|
|
4472
|
+
app.post("/items/:context/chunks/generate/all", async (req, res) => {
|
|
4473
|
+
if (!req.params.context) {
|
|
4474
|
+
res.status(400).json({
|
|
4475
|
+
message: "Missing context in request."
|
|
4476
|
+
});
|
|
4477
|
+
return;
|
|
4478
|
+
}
|
|
4479
|
+
const authenticationResult = await requestValidators.authenticate(req);
|
|
4480
|
+
if (!authenticationResult.user?.id) {
|
|
4481
|
+
res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
|
|
4482
|
+
return;
|
|
4483
|
+
}
|
|
4484
|
+
const user = authenticationResult.user;
|
|
4485
|
+
const context = contexts.find((context2) => context2.id === req.params.context);
|
|
4486
|
+
if (!context) {
|
|
4487
|
+
res.status(400).json({
|
|
4488
|
+
message: "Context not found in registry."
|
|
4489
|
+
});
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
const jobs = await context.embeddings.generate.all(context, user.id, user.role?.id);
|
|
4493
|
+
res.status(200).json({
|
|
4494
|
+
message: "Embeddings generated successfully.",
|
|
4495
|
+
jobs
|
|
4496
|
+
});
|
|
4497
|
+
});
|
|
4498
|
+
app.post("/items/:context/chunks/generate/one/:id", async (req, res) => {
|
|
4499
|
+
if (!req.params.context) {
|
|
4500
|
+
res.status(400).json({
|
|
4501
|
+
message: "Missing context in request."
|
|
4502
|
+
});
|
|
4503
|
+
return;
|
|
4504
|
+
}
|
|
4505
|
+
if (!req.params.id) {
|
|
4506
|
+
res.status(400).json({
|
|
4507
|
+
message: "Missing id in request."
|
|
4508
|
+
});
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4511
|
+
const authenticationResult = await requestValidators.authenticate(req);
|
|
4512
|
+
if (!authenticationResult.user?.id) {
|
|
4513
|
+
res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
|
|
4514
|
+
return;
|
|
4515
|
+
}
|
|
4516
|
+
const user = authenticationResult.user;
|
|
4517
|
+
const context = contexts.find((context2) => context2.id === req.params.context);
|
|
4518
|
+
if (!context) {
|
|
4519
|
+
res.status(400).json({
|
|
4520
|
+
message: "Context not found in registry."
|
|
4521
|
+
});
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
const exists = await context.tableExists();
|
|
4525
|
+
if (!exists) {
|
|
4526
|
+
await context.createItemsTable();
|
|
4527
|
+
}
|
|
4528
|
+
const chunksTableExists = await context.chunksTableExists();
|
|
4529
|
+
if (!chunksTableExists && context.embedder) {
|
|
4530
|
+
await context.createChunksTable();
|
|
4531
|
+
}
|
|
4532
|
+
const { db: db3 } = await postgresClient();
|
|
4533
|
+
const item = await db3.from(getTableName(context.id)).where({ id: req.params.id }).select("*").first();
|
|
4534
|
+
if (!item) {
|
|
4535
|
+
res.status(404).json({
|
|
4536
|
+
message: "Item not found."
|
|
4537
|
+
});
|
|
4538
|
+
return;
|
|
4539
|
+
}
|
|
4540
|
+
const { job } = await context.embeddings.generate.one({
|
|
4541
|
+
item: {
|
|
4542
|
+
...item,
|
|
4543
|
+
id: item.id
|
|
4544
|
+
},
|
|
4545
|
+
user: user.id,
|
|
4546
|
+
role: user.role?.id,
|
|
4547
|
+
trigger: "api"
|
|
4548
|
+
});
|
|
4549
|
+
res.status(200).json({
|
|
4550
|
+
message: job ? "Job scheduled to generate embeddings successfully." : "Embeddings generated successfully.",
|
|
4551
|
+
job
|
|
4552
|
+
});
|
|
4553
|
+
});
|
|
4345
4554
|
app.post("/items/:context/:id", async (req, res) => {
|
|
4346
4555
|
if (!req.params.context) {
|
|
4347
4556
|
res.status(400).json({
|
|
@@ -4387,7 +4596,6 @@ Mood: friendly and intelligent.
|
|
|
4387
4596
|
});
|
|
4388
4597
|
app.post("/items/:context", async (req, res) => {
|
|
4389
4598
|
try {
|
|
4390
|
-
console.log("[EXULU] post items");
|
|
4391
4599
|
if (!req.params.context) {
|
|
4392
4600
|
res.status(400).json({
|
|
4393
4601
|
message: "Missing context in request."
|
|
@@ -4484,6 +4692,10 @@ Mood: friendly and intelligent.
|
|
|
4484
4692
|
if (!exists) {
|
|
4485
4693
|
await context.createItemsTable();
|
|
4486
4694
|
}
|
|
4695
|
+
const chunksTableExists = await context.chunksTableExists();
|
|
4696
|
+
if (!chunksTableExists && context.embedder) {
|
|
4697
|
+
await context.createChunksTable();
|
|
4698
|
+
}
|
|
4487
4699
|
if (req.query.method && !Object.values(VectorMethodEnum).includes(req.query.method)) {
|
|
4488
4700
|
res.status(400).json({
|
|
4489
4701
|
message: "Invalid vector lookup method, must be one of: " + Object.values(VectorMethodEnum).join(", ")
|
|
@@ -4523,11 +4735,6 @@ Mood: friendly and intelligent.
|
|
|
4523
4735
|
agents.forEach((agent) => {
|
|
4524
4736
|
const slug = agent.slug;
|
|
4525
4737
|
if (!slug) return;
|
|
4526
|
-
routeLogs.push({
|
|
4527
|
-
route: slug + "/:instance",
|
|
4528
|
-
method: "POST",
|
|
4529
|
-
note: `Agent endpoint for ${agent.id}`
|
|
4530
|
-
});
|
|
4531
4738
|
app.post(slug + "/:instance", async (req, res) => {
|
|
4532
4739
|
const instance = req.params.instance;
|
|
4533
4740
|
if (!instance) {
|
|
@@ -4649,8 +4856,6 @@ Mood: friendly and intelligent.
|
|
|
4649
4856
|
} else {
|
|
4650
4857
|
console.log("[EXULU] skipping uppy file upload routes, because no S3 compatible region, key or secret is set in the environment.");
|
|
4651
4858
|
}
|
|
4652
|
-
console.log("Routes:");
|
|
4653
|
-
console.table(routeLogs);
|
|
4654
4859
|
const TARGET_API = "https://api.anthropic.com";
|
|
4655
4860
|
app.use("/gateway/anthropic/:id", import_express4.default.raw({ type: "*/*", limit: REQUEST_SIZE_LIMIT }), async (req, res) => {
|
|
4656
4861
|
const path3 = req.url;
|
|
@@ -4995,8 +5200,6 @@ ${code}`
|
|
|
4995
5200
|
}]
|
|
4996
5201
|
})
|
|
4997
5202
|
);
|
|
4998
|
-
console.log("Contexts:");
|
|
4999
|
-
console.table();
|
|
5000
5203
|
};
|
|
5001
5204
|
connect = async () => {
|
|
5002
5205
|
if (!this.express) {
|
|
@@ -5051,14 +5254,6 @@ ${code}`
|
|
|
5051
5254
|
};
|
|
5052
5255
|
this.express.get("/mcp", handleSessionRequest);
|
|
5053
5256
|
this.express.delete("/mcp", handleSessionRequest);
|
|
5054
|
-
const routeLogs = [];
|
|
5055
|
-
routeLogs.push(
|
|
5056
|
-
{ route: "/mcp", method: "GET", note: "Get MCP server status" },
|
|
5057
|
-
{ route: "/mcp", method: "POST", note: "Send MCP request" },
|
|
5058
|
-
{ route: "/mcp", method: "DELETE", note: "Terminate MCP session" }
|
|
5059
|
-
);
|
|
5060
|
-
console.log("MCP Routes:");
|
|
5061
|
-
console.table(routeLogs);
|
|
5062
5257
|
return this.express;
|
|
5063
5258
|
};
|
|
5064
5259
|
};
|
|
@@ -5067,9 +5262,8 @@ ${code}`
|
|
|
5067
5262
|
var import_express8 = __toESM(require("express"), 1);
|
|
5068
5263
|
|
|
5069
5264
|
// src/templates/agents/claude-code.ts
|
|
5070
|
-
var agentId = "0832-5178-1145-2194";
|
|
5071
5265
|
var claudeCodeAgent = new ExuluAgent({
|
|
5072
|
-
id:
|
|
5266
|
+
id: `claude_code_agent`,
|
|
5073
5267
|
name: `Claude Code Agent`,
|
|
5074
5268
|
description: `Claude Code agent, enabling the creation of multiple Claude Code Agent instances with different configurations (rate limits, functions, etc).`,
|
|
5075
5269
|
type: "custom"
|
|
@@ -5077,9 +5271,8 @@ var claudeCodeAgent = new ExuluAgent({
|
|
|
5077
5271
|
|
|
5078
5272
|
// src/templates/agents/claude-opus-4.ts
|
|
5079
5273
|
var import_anthropic = require("@ai-sdk/anthropic");
|
|
5080
|
-
var agentId2 = "5434-5678-9143-2590";
|
|
5081
5274
|
var defaultAgent = new ExuluAgent({
|
|
5082
|
-
id:
|
|
5275
|
+
id: `default_claude_4_opus_agent`,
|
|
5083
5276
|
name: `Default Claude 4 Opus Agent`,
|
|
5084
5277
|
description: `Basic agent without any defined tools, that can support MCP's.`,
|
|
5085
5278
|
type: "agent",
|
|
@@ -5146,7 +5339,7 @@ var logger_default = createLogger;
|
|
|
5146
5339
|
|
|
5147
5340
|
// src/templates/contexts/code-standards.ts
|
|
5148
5341
|
var codeStandardsContext = new ExuluContext({
|
|
5149
|
-
id: "
|
|
5342
|
+
id: "code_standards",
|
|
5150
5343
|
name: "Code Standards",
|
|
5151
5344
|
description: "Code standards that can be used with the Exulu CLI.",
|
|
5152
5345
|
configuration: {
|
|
@@ -5184,6 +5377,13 @@ var projectsContext = new ExuluContext({
|
|
|
5184
5377
|
});
|
|
5185
5378
|
|
|
5186
5379
|
// src/registry/index.ts
|
|
5380
|
+
var isValidPostgresName = (id) => {
|
|
5381
|
+
console.log("[EXULU] validating context id.", id);
|
|
5382
|
+
const regex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
5383
|
+
const isValid = regex.test(id);
|
|
5384
|
+
const length = id.length;
|
|
5385
|
+
return isValid && length <= 80 && length > 5;
|
|
5386
|
+
};
|
|
5187
5387
|
var ExuluApp = class {
|
|
5188
5388
|
_agents = [];
|
|
5189
5389
|
_config;
|
|
@@ -5214,6 +5414,28 @@ var ExuluApp = class {
|
|
|
5214
5414
|
// Add agents as tools
|
|
5215
5415
|
...(agents || []).map((agent) => agent.tool())
|
|
5216
5416
|
];
|
|
5417
|
+
const checks = [
|
|
5418
|
+
...Object.keys(this._contexts || {}).map((x) => ({
|
|
5419
|
+
name: this._contexts?.[x]?.name ?? "",
|
|
5420
|
+
id: this._contexts?.[x]?.id ?? "",
|
|
5421
|
+
type: "context"
|
|
5422
|
+
})),
|
|
5423
|
+
...this._agents.map((agent) => ({
|
|
5424
|
+
name: agent.name ?? "",
|
|
5425
|
+
id: agent.id ?? "",
|
|
5426
|
+
type: "agent"
|
|
5427
|
+
})),
|
|
5428
|
+
...this._tools.map((tool2) => ({
|
|
5429
|
+
name: tool2.name ?? "",
|
|
5430
|
+
id: tool2.id ?? "",
|
|
5431
|
+
type: "tool"
|
|
5432
|
+
}))
|
|
5433
|
+
];
|
|
5434
|
+
const invalid = checks.filter((x) => !isValidPostgresName(x?.id ?? ""));
|
|
5435
|
+
if (invalid.length > 0) {
|
|
5436
|
+
console.error(`%c[EXULU] Invalid ID found for a context, tool or agent: ${invalid.map((x) => x.id).join(", ")}. An ID must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or underscores and be a max length of 80 characters and at least 5 characters long.`, "color: orange; font-weight: bold; \n \n");
|
|
5437
|
+
throw new Error(`Invalid ID found for a context, tool or agent: ${invalid.map((x) => x.id).join(", ")}. An ID must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or underscores and be a max length of 80 characters and at least 5 characters long.`);
|
|
5438
|
+
}
|
|
5217
5439
|
const contextsArray = Object.values(contexts || {});
|
|
5218
5440
|
const queues2 = [
|
|
5219
5441
|
...contextsArray?.length ? contextsArray.map((context) => context.embedder?.queue?.name || null) : []
|
|
@@ -5250,6 +5472,34 @@ var ExuluApp = class {
|
|
|
5250
5472
|
get agents() {
|
|
5251
5473
|
return this._agents;
|
|
5252
5474
|
}
|
|
5475
|
+
embeddings = {
|
|
5476
|
+
generate: {
|
|
5477
|
+
one: async ({
|
|
5478
|
+
context: contextId,
|
|
5479
|
+
item: itemId
|
|
5480
|
+
}) => {
|
|
5481
|
+
const { db: db3 } = await postgresClient();
|
|
5482
|
+
const item = await db3.from(getTableName(contextId)).where({ id: itemId }).select("*").first();
|
|
5483
|
+
const context = this.contexts.find((x) => contextId === x.id);
|
|
5484
|
+
if (!context) {
|
|
5485
|
+
throw new Error(`Context ${contextId} not found in registry.`);
|
|
5486
|
+
}
|
|
5487
|
+
return await context.embeddings.generate.one({
|
|
5488
|
+
item,
|
|
5489
|
+
trigger: "api"
|
|
5490
|
+
});
|
|
5491
|
+
},
|
|
5492
|
+
all: async ({
|
|
5493
|
+
context: contextId
|
|
5494
|
+
}) => {
|
|
5495
|
+
const context = this.contexts.find((x) => contextId === x.id);
|
|
5496
|
+
if (!context) {
|
|
5497
|
+
throw new Error(`Context ${contextId} not found in registry.`);
|
|
5498
|
+
}
|
|
5499
|
+
return await context.embeddings.generate.all(context, void 0, void 0);
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
};
|
|
5253
5503
|
bullmq = {
|
|
5254
5504
|
workers: {
|
|
5255
5505
|
create: async () => {
|