@exulu/backend 1.67.0 → 1.69.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/{chunk-ZPZKOT6I.js → chunk-IVC2M56U.js} +1562 -163
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-4B7BQ5G2.js → convert-exulu-tools-to-ai-sdk-tools-ET6UI7YG.js} +1 -1
- package/dist/index.cjs +24847 -22818
- package/dist/index.d.cts +315 -101
- package/dist/index.d.ts +315 -101
- package/dist/index.js +2766 -2232
- package/ee/agentic-retrieval/v3/agent-loop.ts +4 -4
- package/ee/agentic-retrieval/v3/index.ts +20 -6
- package/ee/python/documents/processing/doc_processor.ts +136 -40
- package/ee/python/documents/processing/split_pdf.py +97 -0
- package/ee/queues/queues.ts +10 -0
- package/ee/queues/redis-startup.ts +121 -0
- package/ee/workers.ts +9 -17
- package/package.json +1 -1
- package/ee/agentic-retrieval/v4/agent-loop.ts +0 -208
- package/ee/agentic-retrieval/v4/context-sampler.ts +0 -79
- package/ee/agentic-retrieval/v4/index.ts +0 -690
- package/ee/agentic-retrieval/v4/types.ts +0 -58
|
@@ -119,9 +119,9 @@ async function postgresClient() {
|
|
|
119
119
|
// Log pool events to help debug connection issues
|
|
120
120
|
afterCreate: (conn, done) => {
|
|
121
121
|
console.log("[EXULU] New database connection created");
|
|
122
|
-
conn.query("SET statement_timeout = 1800000", (err) => {
|
|
122
|
+
conn.query("SET statement_timeout = 1800000; SET hnsw.ef_search = 20", (err) => {
|
|
123
123
|
if (err) {
|
|
124
|
-
console.error("[EXULU] Error setting
|
|
124
|
+
console.error("[EXULU] Error setting connection parameters:", err);
|
|
125
125
|
}
|
|
126
126
|
done(err, conn);
|
|
127
127
|
});
|
|
@@ -362,9 +362,14 @@ var supervise = async (cfg) => {
|
|
|
362
362
|
}
|
|
363
363
|
};
|
|
364
364
|
var _packageRoot;
|
|
365
|
+
var _clientMode = false;
|
|
365
366
|
var setLiteLLMPackageRoot = (root) => {
|
|
366
367
|
_packageRoot = root;
|
|
367
368
|
};
|
|
369
|
+
var enableLiteLLMClientMode = () => {
|
|
370
|
+
if (internal.readyPromise) return;
|
|
371
|
+
_clientMode = true;
|
|
372
|
+
};
|
|
368
373
|
var startLiteLLMSupervisor = async (options = {}) => {
|
|
369
374
|
if (!isLiteLLMEnabled()) return;
|
|
370
375
|
if (internal.readyPromise) {
|
|
@@ -413,6 +418,27 @@ var startLiteLLMSupervisor = async (options = {}) => {
|
|
|
413
418
|
};
|
|
414
419
|
var waitForLiteLLMReady = async () => {
|
|
415
420
|
if (!isLiteLLMEnabled()) return;
|
|
421
|
+
if (_clientMode) {
|
|
422
|
+
if (internal.state === "ready") return;
|
|
423
|
+
const host = process.env.LITELLM_HOST ?? "127.0.0.1";
|
|
424
|
+
const port = process.env.LITELLM_PORT ?? "4000";
|
|
425
|
+
const url = `http://${host}:${port}/health/liveliness`;
|
|
426
|
+
let res;
|
|
427
|
+
try {
|
|
428
|
+
res = await fetch(url, { method: "GET" });
|
|
429
|
+
} catch (err) {
|
|
430
|
+
throw new Error(
|
|
431
|
+
`LiteLLM proxy not reachable at ${url} (is the Exulu server process running?): ${err.message}`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (!res.ok) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
`LiteLLM proxy health probe at ${url} returned ${res.status}.`
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
internal.state = "ready";
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
416
442
|
if (!internal.readyPromise) {
|
|
417
443
|
return startLiteLLMSupervisor();
|
|
418
444
|
}
|
|
@@ -499,6 +525,12 @@ function buildTags(input) {
|
|
|
499
525
|
if (input.routine_name) {
|
|
500
526
|
candidates.push("routine_name_" + input.routine_name);
|
|
501
527
|
}
|
|
528
|
+
if (input.context_id) {
|
|
529
|
+
candidates.push("context_id_" + input.context_id);
|
|
530
|
+
}
|
|
531
|
+
if (input.context_name) {
|
|
532
|
+
candidates.push("context_name_" + input.context_name);
|
|
533
|
+
}
|
|
502
534
|
console.log("[EXULU] Candidates", candidates);
|
|
503
535
|
const out = [];
|
|
504
536
|
for (const candidate of candidates) {
|
|
@@ -851,11 +883,20 @@ function durationToDays(duration) {
|
|
|
851
883
|
}
|
|
852
884
|
function windowStartYmd(reset_at, duration) {
|
|
853
885
|
const days = durationToDays(duration);
|
|
886
|
+
const windowMs = days * DAY_MS;
|
|
887
|
+
const now = Date.now();
|
|
854
888
|
const reset = reset_at ? new Date(reset_at) : null;
|
|
855
|
-
const
|
|
856
|
-
const trailingStart = new Date(
|
|
857
|
-
|
|
858
|
-
|
|
889
|
+
const resetMs = reset && !Number.isNaN(reset.getTime()) ? reset.getTime() : null;
|
|
890
|
+
const trailingStart = new Date(now - windowMs);
|
|
891
|
+
if (resetMs !== null) {
|
|
892
|
+
if (resetMs > now) {
|
|
893
|
+
const periodStart = new Date(resetMs - windowMs);
|
|
894
|
+
return ymd(periodStart > trailingStart ? periodStart : trailingStart);
|
|
895
|
+
} else {
|
|
896
|
+
return ymd(reset > trailingStart ? reset : trailingStart);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return ymd(trailingStart);
|
|
859
900
|
}
|
|
860
901
|
async function enrichSpendFromActivity(map) {
|
|
861
902
|
const names = Object.keys(map);
|
|
@@ -866,7 +907,10 @@ async function enrichSpendFromActivity(map) {
|
|
|
866
907
|
windows[name] = windowStartYmd(ti.budget_reset_at, ti.budget_duration);
|
|
867
908
|
}
|
|
868
909
|
try {
|
|
869
|
-
const spendByTag = await getTagSpendByWindow(
|
|
910
|
+
const spendByTag = await getTagSpendByWindow(
|
|
911
|
+
windows,
|
|
912
|
+
ymd(new Date(Date.now() + DAY_MS))
|
|
913
|
+
);
|
|
870
914
|
for (const name of names) {
|
|
871
915
|
const spend = spendByTag[name];
|
|
872
916
|
if (typeof spend === "number" && Number.isFinite(spend)) {
|
|
@@ -1002,6 +1046,7 @@ async function getUserBudgetView(userId) {
|
|
|
1002
1046
|
readCache.set(tag, { expiry: Date.now() + READ_TTL_MS, view: null });
|
|
1003
1047
|
return null;
|
|
1004
1048
|
}
|
|
1049
|
+
await provisionDefaultUserBudget(userId);
|
|
1005
1050
|
const info = await tagInfo([tag]);
|
|
1006
1051
|
const ti = info[tag];
|
|
1007
1052
|
if (ti?.max_budget != null) {
|
|
@@ -1090,7 +1135,12 @@ var getLiteLLMProvider = ({
|
|
|
1090
1135
|
// supports — including Vertex Gemini, which translates it into
|
|
1091
1136
|
// responseSchema/responseMimeType — so enabling this matches the actual
|
|
1092
1137
|
// proxy contract.
|
|
1093
|
-
supportsStructuredOutputs: true
|
|
1138
|
+
supportsStructuredOutputs: true,
|
|
1139
|
+
// Request token usage on STREAMED responses. Without this the openai-compatible
|
|
1140
|
+
// provider omits `stream_options: { include_usage: true }`, so LiteLLM returns no
|
|
1141
|
+
// usage for streaming calls — which zeroes out the per-request token metrics and
|
|
1142
|
+
// the message-footer token count (both read the AI SDK `totalUsage`/finish-part usage).
|
|
1143
|
+
includeUsage: true
|
|
1094
1144
|
});
|
|
1095
1145
|
};
|
|
1096
1146
|
async function resolveModel(input) {
|
|
@@ -1109,7 +1159,10 @@ async function resolveModel(input) {
|
|
|
1109
1159
|
const litellm = getLiteLLMProvider({
|
|
1110
1160
|
user,
|
|
1111
1161
|
role: user?.role,
|
|
1112
|
-
project
|
|
1162
|
+
// Fall back to the caller's own project (set on API keys) when no
|
|
1163
|
+
// explicit request project is supplied, so API-triggered requests are
|
|
1164
|
+
// attributed to the key's project.
|
|
1165
|
+
project: project ?? user?.project,
|
|
1113
1166
|
agent,
|
|
1114
1167
|
team: user?.team,
|
|
1115
1168
|
routine
|
|
@@ -1568,7 +1621,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1568
1621
|
});
|
|
1569
1622
|
providerapikey = resolved.apiKey;
|
|
1570
1623
|
}
|
|
1571
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-
|
|
1624
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-ET6UI7YG.js");
|
|
1572
1625
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
1573
1626
|
[this],
|
|
1574
1627
|
[],
|
|
@@ -1577,7 +1630,6 @@ var ExuluTool = class _ExuluTool {
|
|
|
1577
1630
|
agent.tools,
|
|
1578
1631
|
providerapikey,
|
|
1579
1632
|
void 0,
|
|
1580
|
-
void 0,
|
|
1581
1633
|
user,
|
|
1582
1634
|
config,
|
|
1583
1635
|
void 0,
|
|
@@ -1843,6 +1895,111 @@ var createSessionItemsRetrievalTool = async ({
|
|
|
1843
1895
|
import { z as z9 } from "zod";
|
|
1844
1896
|
import { createBashTool } from "bash-tool";
|
|
1845
1897
|
|
|
1898
|
+
// src/exulu/resolve-reranker.ts
|
|
1899
|
+
import "fs";
|
|
1900
|
+
var ResolveRerankerError = class extends Error {
|
|
1901
|
+
constructor(code, message) {
|
|
1902
|
+
super(message);
|
|
1903
|
+
this.code = code;
|
|
1904
|
+
this.name = "ResolveRerankerError";
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
async function resolveReranker(input) {
|
|
1908
|
+
const { model, contextId, contextName, user, userId, roleId, project, agent, routine } = input;
|
|
1909
|
+
if (!isLiteLLMEnabled()) {
|
|
1910
|
+
throw new ResolveRerankerError(
|
|
1911
|
+
"LITELLM_NOT_CONFIGURED",
|
|
1912
|
+
"resolveReranker requires EXULU_USE_LITELLM=true \u2014 reranking is served exclusively through the LiteLLM proxy."
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
try {
|
|
1916
|
+
await waitForLiteLLMReady();
|
|
1917
|
+
} catch (err) {
|
|
1918
|
+
throw new ResolveRerankerError(
|
|
1919
|
+
"LITELLM_NOT_READY",
|
|
1920
|
+
`LiteLLM is not ready: ${err.message}`
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
const host = process.env.LITELLM_HOST ?? "127.0.0.1";
|
|
1924
|
+
const port = process.env.LITELLM_PORT ?? "4000";
|
|
1925
|
+
const masterKey = process.env.LITELLM_MASTER_KEY;
|
|
1926
|
+
if (!masterKey) {
|
|
1927
|
+
throw new ResolveRerankerError(
|
|
1928
|
+
"LITELLM_NOT_CONFIGURED",
|
|
1929
|
+
"LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
const resolvedUserId = user?.id ?? userId;
|
|
1933
|
+
if (resolvedUserId) await provisionDefaultUserBudget(resolvedUserId);
|
|
1934
|
+
const role = user?.role;
|
|
1935
|
+
const tags = buildTags({
|
|
1936
|
+
user_id: resolvedUserId,
|
|
1937
|
+
role_id: role?.id ?? roleId,
|
|
1938
|
+
project_id: (project ?? user?.project)?.id,
|
|
1939
|
+
agent_id: agent?.id,
|
|
1940
|
+
team_id: user?.team?.id,
|
|
1941
|
+
routine_id: routine?.id,
|
|
1942
|
+
context_id: contextId,
|
|
1943
|
+
user_name: !user ? void 0 : user.type === "api" ? user.firstname ?? user.email : user.email,
|
|
1944
|
+
role_name: role?.name,
|
|
1945
|
+
project_name: (project ?? user?.project)?.name,
|
|
1946
|
+
agent_name: agent?.name,
|
|
1947
|
+
team_name: user?.team?.name,
|
|
1948
|
+
routine_name: routine?.name,
|
|
1949
|
+
context_name: contextName
|
|
1950
|
+
});
|
|
1951
|
+
const endpoint = `http://${host}:${port}/v1/rerank`;
|
|
1952
|
+
const rerank = async (query, chunks, opts) => {
|
|
1953
|
+
try {
|
|
1954
|
+
if (chunks.length === 0) return [];
|
|
1955
|
+
const documents = chunks.map(
|
|
1956
|
+
(c) => `${c.item_name ?? ""}: ${c.chunk_content ?? ""}`
|
|
1957
|
+
);
|
|
1958
|
+
const res = await fetch(endpoint, {
|
|
1959
|
+
method: "POST",
|
|
1960
|
+
headers: {
|
|
1961
|
+
Authorization: `Bearer ${masterKey}`,
|
|
1962
|
+
"Content-Type": "application/json"
|
|
1963
|
+
},
|
|
1964
|
+
body: JSON.stringify({
|
|
1965
|
+
model,
|
|
1966
|
+
query,
|
|
1967
|
+
documents,
|
|
1968
|
+
// Default to scoring every document so callers can fully reorder; a
|
|
1969
|
+
// caller can pass a smaller topN as an optimization hint.
|
|
1970
|
+
top_n: opts?.topN ?? documents.length,
|
|
1971
|
+
// LiteLLM reads metadata.tags for tag-based spend tracking.
|
|
1972
|
+
metadata: { tags }
|
|
1973
|
+
})
|
|
1974
|
+
});
|
|
1975
|
+
if (!res.ok) {
|
|
1976
|
+
const text = await res.text().catch(() => "");
|
|
1977
|
+
throw new Error(
|
|
1978
|
+
`[EXULU] LiteLLM /v1/rerank returned ${res.status} for model "${model}": ${text}`
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
const json = await res.json();
|
|
1982
|
+
if (!Array.isArray(json.results)) {
|
|
1983
|
+
throw new Error(
|
|
1984
|
+
`[EXULU] LiteLLM /v1/rerank returned no results for model "${model}".`
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
const reranked = json.results.filter(
|
|
1988
|
+
(r) => typeof r.index === "number" && !!chunks[r.index]
|
|
1989
|
+
).map((r) => ({
|
|
1990
|
+
...chunks[r.index],
|
|
1991
|
+
rerank_score: r.relevance_score ?? 0
|
|
1992
|
+
}));
|
|
1993
|
+
reranked.sort((a, b) => b.rerank_score - a.rerank_score);
|
|
1994
|
+
return reranked;
|
|
1995
|
+
} catch (err) {
|
|
1996
|
+
console.error("[EXULU] Error reranking:", err);
|
|
1997
|
+
return [];
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
return { model, rerank };
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1846
2003
|
// ee/entitlements.ts
|
|
1847
2004
|
var ENTITLEMENTS = {
|
|
1848
2005
|
"rbac": false,
|
|
@@ -1895,7 +2052,7 @@ async function withRetry(generateFn, maxRetries = 3) {
|
|
|
1895
2052
|
if (attempt === maxRetries) {
|
|
1896
2053
|
throw error;
|
|
1897
2054
|
}
|
|
1898
|
-
await new Promise((
|
|
2055
|
+
await new Promise((resolve4) => setTimeout(resolve4, Math.pow(2, attempt) * 1e3));
|
|
1899
2056
|
}
|
|
1900
2057
|
}
|
|
1901
2058
|
throw lastError;
|
|
@@ -2037,6 +2194,12 @@ var authentication = async ({
|
|
|
2037
2194
|
user.team = team;
|
|
2038
2195
|
}
|
|
2039
2196
|
}
|
|
2197
|
+
if (user?.project) {
|
|
2198
|
+
const project = await db2.from("projects").select("*").where("id", user?.project).first();
|
|
2199
|
+
if (project) {
|
|
2200
|
+
user.project = project;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2040
2203
|
if (!user) {
|
|
2041
2204
|
return {
|
|
2042
2205
|
error: true,
|
|
@@ -2102,6 +2265,18 @@ var authentication = async ({
|
|
|
2102
2265
|
user.role = role;
|
|
2103
2266
|
}
|
|
2104
2267
|
}
|
|
2268
|
+
if (user?.team) {
|
|
2269
|
+
const team = await db2.from("teams").select("*").where("id", user?.team).first();
|
|
2270
|
+
if (team) {
|
|
2271
|
+
user.team = team;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
if (user?.project) {
|
|
2275
|
+
const project = await db2.from("projects").select("*").where("id", user?.project).first();
|
|
2276
|
+
if (project) {
|
|
2277
|
+
user.project = project;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2105
2280
|
return {
|
|
2106
2281
|
error: false,
|
|
2107
2282
|
code: 200,
|
|
@@ -2270,7 +2445,7 @@ var uploadFile = async (file, fileName, config, options = {}, user, customBucket
|
|
|
2270
2445
|
if (error.name === "SignatureDoesNotMatch" || error.name === "InvalidAccessKeyId" || error.name === "AccessDenied") {
|
|
2271
2446
|
if (attempt < maxRetries) {
|
|
2272
2447
|
const backoffMs = Math.pow(2, attempt) * 1e3;
|
|
2273
|
-
await new Promise((
|
|
2448
|
+
await new Promise((resolve4) => setTimeout(resolve4, backoffMs));
|
|
2274
2449
|
s3Client = void 0;
|
|
2275
2450
|
getS3Client(config);
|
|
2276
2451
|
continue;
|
|
@@ -2991,9 +3166,871 @@ var ExuluStorage = class {
|
|
|
2991
3166
|
// todo add upload and delete methods
|
|
2992
3167
|
};
|
|
2993
3168
|
|
|
3169
|
+
// src/exulu/table-names.ts
|
|
3170
|
+
var getTableName = (id) => sanitizeName(id) + "_items";
|
|
3171
|
+
var getChunksTableName = (id) => sanitizeName(id) + "_chunks";
|
|
3172
|
+
|
|
2994
3173
|
// src/exulu/context.ts
|
|
2995
3174
|
import pgvector2 from "pgvector/knex";
|
|
2996
3175
|
|
|
3176
|
+
// ee/tokenizer.ts
|
|
3177
|
+
import { Tiktoken } from "tiktoken/lite";
|
|
3178
|
+
import { load } from "tiktoken/load";
|
|
3179
|
+
import registry2 from "tiktoken/registry.json" with { type: "json" };
|
|
3180
|
+
import models from "tiktoken/model_to_encoding.json" with { type: "json" };
|
|
3181
|
+
var ExuluTokenizer = class {
|
|
3182
|
+
constructor() {
|
|
3183
|
+
}
|
|
3184
|
+
encoder = null;
|
|
3185
|
+
async create(modelName) {
|
|
3186
|
+
if (this.encoder) {
|
|
3187
|
+
return this.encoder;
|
|
3188
|
+
}
|
|
3189
|
+
const time = performance.now();
|
|
3190
|
+
console.log("[EXULU] Loading tokenizer.", modelName);
|
|
3191
|
+
const model = await load(registry2[models[modelName]]);
|
|
3192
|
+
console.log("[EXULU] Loaded tokenizer.", modelName, performance.now() - time);
|
|
3193
|
+
const encoder = new Tiktoken(model.bpe_ranks, model.special_tokens, model.pat_str);
|
|
3194
|
+
console.log("[EXULU] Set encoder.");
|
|
3195
|
+
this.encoder = encoder;
|
|
3196
|
+
return encoder;
|
|
3197
|
+
}
|
|
3198
|
+
async decode(tokens) {
|
|
3199
|
+
if (!this.encoder) {
|
|
3200
|
+
throw new Error("Tokenizer not initialized");
|
|
3201
|
+
}
|
|
3202
|
+
const text = this.encoder.decode(tokens);
|
|
3203
|
+
return new TextDecoder().decode(text);
|
|
3204
|
+
}
|
|
3205
|
+
async decodeBatch(tokenSequences) {
|
|
3206
|
+
if (!this.encoder) {
|
|
3207
|
+
throw new Error("Tokenizer not initialized");
|
|
3208
|
+
}
|
|
3209
|
+
const promises = tokenSequences.map((tokens) => this.decode(tokens));
|
|
3210
|
+
return await Promise.all(promises);
|
|
3211
|
+
}
|
|
3212
|
+
encode(text) {
|
|
3213
|
+
if (!this.encoder) {
|
|
3214
|
+
throw new Error("Tokenizer not initialized");
|
|
3215
|
+
}
|
|
3216
|
+
const time = performance.now();
|
|
3217
|
+
console.log("[EXULU] Encoding text length: " + (text?.length || 0));
|
|
3218
|
+
const tokens = this.encoder.encode(text);
|
|
3219
|
+
console.log("[EXULU] Finished encoding text.", performance.now() - time);
|
|
3220
|
+
return tokens;
|
|
3221
|
+
}
|
|
3222
|
+
async countTokensBatch(texts) {
|
|
3223
|
+
if (!this.encoder) {
|
|
3224
|
+
throw new Error("Tokenizer not initialized");
|
|
3225
|
+
}
|
|
3226
|
+
const promises = texts.map((text) => Promise.resolve(this.countTokens(text)));
|
|
3227
|
+
return await Promise.all(promises);
|
|
3228
|
+
}
|
|
3229
|
+
countTokens(text) {
|
|
3230
|
+
if (!this.encoder) {
|
|
3231
|
+
throw new Error("Tokenizer not initialized");
|
|
3232
|
+
}
|
|
3233
|
+
const tokens = this.encoder.encode(text);
|
|
3234
|
+
const count = tokens.length;
|
|
3235
|
+
console.log("[EXULU] Token count.", count);
|
|
3236
|
+
return count;
|
|
3237
|
+
}
|
|
3238
|
+
async free() {
|
|
3239
|
+
console.log("[EXULU] Freeing tokenizer.");
|
|
3240
|
+
if (this.encoder) {
|
|
3241
|
+
this.encoder.free();
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
};
|
|
3245
|
+
|
|
3246
|
+
// src/chunking/types/base.ts
|
|
3247
|
+
var Chunk = class _Chunk {
|
|
3248
|
+
/** The text of the chunk. */
|
|
3249
|
+
text;
|
|
3250
|
+
/** The starting index of the chunk in the original text. */
|
|
3251
|
+
startIndex;
|
|
3252
|
+
/** The ending index of the chunk in the original text. */
|
|
3253
|
+
endIndex;
|
|
3254
|
+
/** The number of tokens in the chunk. */
|
|
3255
|
+
tokenCount;
|
|
3256
|
+
/** Optional embedding for the chunk. */
|
|
3257
|
+
embedding;
|
|
3258
|
+
/**
|
|
3259
|
+
* Constructs a new Chunk object.
|
|
3260
|
+
*
|
|
3261
|
+
* @param {ChunkData} data - The data to construct the Chunk from.
|
|
3262
|
+
*/
|
|
3263
|
+
constructor(data) {
|
|
3264
|
+
this.text = data.text;
|
|
3265
|
+
this.startIndex = data.startIndex;
|
|
3266
|
+
this.endIndex = data.endIndex;
|
|
3267
|
+
this.tokenCount = data.tokenCount;
|
|
3268
|
+
this.embedding = data.embedding;
|
|
3269
|
+
if (this.startIndex > this.endIndex) {
|
|
3270
|
+
throw new Error("Start index must be less than or equal to end index.");
|
|
3271
|
+
}
|
|
3272
|
+
if (this.tokenCount < 0) {
|
|
3273
|
+
throw new Error("Token count must be a non-negative integer.");
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
/** Return a string representation of the Chunk.
|
|
3277
|
+
*
|
|
3278
|
+
* @returns {string} The text of the chunk.
|
|
3279
|
+
*/
|
|
3280
|
+
toString() {
|
|
3281
|
+
return this.text;
|
|
3282
|
+
}
|
|
3283
|
+
/** Return a detailed string representation of the Chunk.
|
|
3284
|
+
*
|
|
3285
|
+
* @returns {string} The detailed string representation of the Chunk.
|
|
3286
|
+
*/
|
|
3287
|
+
toRepresentation() {
|
|
3288
|
+
let repr = `Chunk(text='${this.text}', tokenCount=${this.tokenCount}, startIndex=${this.startIndex}, endIndex=${this.endIndex}`;
|
|
3289
|
+
repr += ")";
|
|
3290
|
+
return repr;
|
|
3291
|
+
}
|
|
3292
|
+
/** Return a slice of the chunk's text.
|
|
3293
|
+
*
|
|
3294
|
+
* @param {number} [start] - The starting index of the slice.
|
|
3295
|
+
* @param {number} [end] - The ending index of the slice.
|
|
3296
|
+
* @returns {string} The slice of the chunk's text.
|
|
3297
|
+
*/
|
|
3298
|
+
slice(start, end) {
|
|
3299
|
+
return this.text.slice(start, end);
|
|
3300
|
+
}
|
|
3301
|
+
/** Return the Chunk as a dictionary-like object.
|
|
3302
|
+
*
|
|
3303
|
+
* @returns {ChunkData} The dictionary-like object.
|
|
3304
|
+
*/
|
|
3305
|
+
toDict() {
|
|
3306
|
+
return {
|
|
3307
|
+
text: this.text,
|
|
3308
|
+
startIndex: this.startIndex,
|
|
3309
|
+
endIndex: this.endIndex,
|
|
3310
|
+
tokenCount: this.tokenCount,
|
|
3311
|
+
embedding: this.embedding
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
/** Create a Chunk object from a dictionary-like object.
|
|
3315
|
+
*
|
|
3316
|
+
* @param {ChunkData} data - The dictionary-like object.
|
|
3317
|
+
* @returns {Chunk} The Chunk object.
|
|
3318
|
+
*/
|
|
3319
|
+
static fromDict(data) {
|
|
3320
|
+
return new _Chunk({
|
|
3321
|
+
text: data.text,
|
|
3322
|
+
startIndex: data.startIndex,
|
|
3323
|
+
endIndex: data.endIndex,
|
|
3324
|
+
tokenCount: data.tokenCount,
|
|
3325
|
+
embedding: data.embedding
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
/** Return a deep copy of the chunk.
|
|
3329
|
+
*
|
|
3330
|
+
* @returns {Chunk} The deep copy of the chunk.
|
|
3331
|
+
*/
|
|
3332
|
+
copy() {
|
|
3333
|
+
return _Chunk.fromDict(this.toDict());
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
|
|
3337
|
+
// src/chunking/types/sentence.ts
|
|
3338
|
+
var Sentence = class _Sentence {
|
|
3339
|
+
/** The text of the sentence */
|
|
3340
|
+
text;
|
|
3341
|
+
/** The starting index of the sentence in the original text */
|
|
3342
|
+
startIndex;
|
|
3343
|
+
/** The ending index of the sentence in the original text */
|
|
3344
|
+
endIndex;
|
|
3345
|
+
/** The number of tokens in the sentence */
|
|
3346
|
+
tokenCount;
|
|
3347
|
+
constructor(data) {
|
|
3348
|
+
this.text = data.text;
|
|
3349
|
+
this.startIndex = data.startIndex;
|
|
3350
|
+
this.endIndex = data.endIndex;
|
|
3351
|
+
this.tokenCount = data.tokenCount;
|
|
3352
|
+
}
|
|
3353
|
+
/** Return a string representation of the Sentence */
|
|
3354
|
+
toString() {
|
|
3355
|
+
return `Sentence(text=${this.text}, startIndex=${this.startIndex}, endIndex=${this.endIndex}, tokenCount=${this.tokenCount})`;
|
|
3356
|
+
}
|
|
3357
|
+
/** Return the Sentence as a dictionary-like object */
|
|
3358
|
+
toDict() {
|
|
3359
|
+
return {
|
|
3360
|
+
text: this.text,
|
|
3361
|
+
startIndex: this.startIndex,
|
|
3362
|
+
endIndex: this.endIndex,
|
|
3363
|
+
tokenCount: this.tokenCount
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
3366
|
+
/** Create a Sentence object from a dictionary-like object */
|
|
3367
|
+
static fromDict(data) {
|
|
3368
|
+
return new _Sentence(data);
|
|
3369
|
+
}
|
|
3370
|
+
};
|
|
3371
|
+
var SentenceChunk = class _SentenceChunk extends Chunk {
|
|
3372
|
+
/** List of sentences in the chunk */
|
|
3373
|
+
sentences;
|
|
3374
|
+
constructor(data) {
|
|
3375
|
+
super(data);
|
|
3376
|
+
this.sentences = data.sentences;
|
|
3377
|
+
this.embedding = data.embedding ?? void 0;
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Returns a detailed string representation of the SentenceChunk, including its text, start and end indices, token count, and a list of all contained sentences with their metadata.
|
|
3381
|
+
*
|
|
3382
|
+
* This method overrides the base {@link Chunk} toString method to provide a more informative output, which is especially useful for debugging and logging. Each sentence in the chunk is represented using its own toString method, and all sentences are included in the output.
|
|
3383
|
+
*
|
|
3384
|
+
* @returns {string} A string describing the SentenceChunk and all its sentences, e.g.,
|
|
3385
|
+
* SentenceChunk(text=..., startIndex=..., endIndex=..., tokenCount=..., sentences=[Sentence(...), ...])
|
|
3386
|
+
*/
|
|
3387
|
+
toString() {
|
|
3388
|
+
const sentencesStr = this.sentences.map((s) => s.toString()).join(", ");
|
|
3389
|
+
return `SentenceChunk(text=${this.text}, startIndex=${this.startIndex}, endIndex=${this.endIndex}, tokenCount=${this.tokenCount}, sentences=[${sentencesStr}])`;
|
|
3390
|
+
}
|
|
3391
|
+
/**
|
|
3392
|
+
* Returns the SentenceChunk as a dictionary-like object.
|
|
3393
|
+
*
|
|
3394
|
+
* This method extends the base {@link Chunk} toDict method to include the sentences in the chunk.
|
|
3395
|
+
*
|
|
3396
|
+
* @returns {SentenceChunkData} A dictionary-like object containing the chunk's text, start and end indices, token count, and an array of sentence data.
|
|
3397
|
+
/** Return the SentenceChunk as a dictionary-like object */
|
|
3398
|
+
toDict() {
|
|
3399
|
+
const baseDict = super.toDict();
|
|
3400
|
+
return {
|
|
3401
|
+
...baseDict,
|
|
3402
|
+
sentences: this.sentences.map((sentence) => sentence.toDict())
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
/**
|
|
3406
|
+
* Creates a SentenceChunk object from a dictionary-like object.
|
|
3407
|
+
*
|
|
3408
|
+
* This method extends the base {@link Chunk} fromDict method to include the sentences in the chunk.
|
|
3409
|
+
*
|
|
3410
|
+
* @param {SentenceChunkData} data - A dictionary-like object containing the chunk's text, start and end indices, token count, and an array of sentence data.
|
|
3411
|
+
* @returns {SentenceChunk} A new SentenceChunk object created from the provided dictionary-like object.
|
|
3412
|
+
*/
|
|
3413
|
+
static fromDict(data) {
|
|
3414
|
+
const sentences = data.sentences.map((sentence) => Sentence.fromDict(sentence));
|
|
3415
|
+
return new _SentenceChunk({
|
|
3416
|
+
text: data.text,
|
|
3417
|
+
startIndex: data.startIndex,
|
|
3418
|
+
endIndex: data.endIndex,
|
|
3419
|
+
tokenCount: data.tokenCount,
|
|
3420
|
+
sentences,
|
|
3421
|
+
embedding: data.embedding ?? void 0
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3424
|
+
};
|
|
3425
|
+
|
|
3426
|
+
// src/chunking/base.ts
|
|
3427
|
+
var BaseChunker = class {
|
|
3428
|
+
tokenizer;
|
|
3429
|
+
_useConcurrency = true;
|
|
3430
|
+
// Determines if batch processing uses Promise.all
|
|
3431
|
+
constructor(tokenizer) {
|
|
3432
|
+
this.tokenizer = tokenizer;
|
|
3433
|
+
}
|
|
3434
|
+
/**
|
|
3435
|
+
* Returns a string representation of the chunker instance.
|
|
3436
|
+
*
|
|
3437
|
+
* @returns {string} The class name and constructor signature.
|
|
3438
|
+
*/
|
|
3439
|
+
toString() {
|
|
3440
|
+
return `${this.constructor.name}()`;
|
|
3441
|
+
}
|
|
3442
|
+
async call(textOrTexts, showProgress = false) {
|
|
3443
|
+
if (typeof textOrTexts === "string") {
|
|
3444
|
+
return this.chunk(textOrTexts);
|
|
3445
|
+
} else if (Array.isArray(textOrTexts)) {
|
|
3446
|
+
return this.chunkBatch(textOrTexts, showProgress);
|
|
3447
|
+
} else {
|
|
3448
|
+
throw new Error("Input must be a string or an array of strings.");
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
/**
|
|
3452
|
+
* Process a batch of texts sequentially (one after another).
|
|
3453
|
+
*
|
|
3454
|
+
* @protected
|
|
3455
|
+
* @param {string[]} texts - The texts to chunk.
|
|
3456
|
+
* @param {boolean} [showProgress=false] - Whether to display progress in the console.
|
|
3457
|
+
* @returns {Promise<Chunk[][]>} An array of chunked results for each input text.
|
|
3458
|
+
*/
|
|
3459
|
+
async _sequential_batch_processing(texts, showProgress = false) {
|
|
3460
|
+
const results = [];
|
|
3461
|
+
const total = texts.length;
|
|
3462
|
+
for (let i = 0; i < total; i++) {
|
|
3463
|
+
if (showProgress && total > 1) {
|
|
3464
|
+
const progress = Math.round((i + 1) / total * 100);
|
|
3465
|
+
process.stdout.write(`Sequential processing: Document ${i + 1}/${total} (${progress}%)\r`);
|
|
3466
|
+
}
|
|
3467
|
+
results.push(await this.chunk(texts[i]));
|
|
3468
|
+
}
|
|
3469
|
+
if (showProgress && total > 1) {
|
|
3470
|
+
process.stdout.write("\n");
|
|
3471
|
+
}
|
|
3472
|
+
return results;
|
|
3473
|
+
}
|
|
3474
|
+
/**
|
|
3475
|
+
* Process a batch of texts concurrently using Promise.all.
|
|
3476
|
+
*
|
|
3477
|
+
* @protected
|
|
3478
|
+
* @param {string[]} texts - The texts to chunk.
|
|
3479
|
+
* @param {boolean} [showProgress=false] - Whether to display progress in the console.
|
|
3480
|
+
* @returns {Promise<Chunk[][]>} An array of chunked results for each input text.
|
|
3481
|
+
*/
|
|
3482
|
+
async _concurrent_batch_processing(texts, showProgress = false) {
|
|
3483
|
+
const total = texts.length;
|
|
3484
|
+
let completedCount = 0;
|
|
3485
|
+
const updateProgress = () => {
|
|
3486
|
+
if (showProgress && total > 1) {
|
|
3487
|
+
completedCount++;
|
|
3488
|
+
const progress = Math.round(completedCount / total * 100);
|
|
3489
|
+
process.stdout.write(
|
|
3490
|
+
`Concurrent processing: Document ${completedCount}/${total} (${progress}%)\r`
|
|
3491
|
+
);
|
|
3492
|
+
}
|
|
3493
|
+
};
|
|
3494
|
+
const chunkPromises = texts.map(
|
|
3495
|
+
(text) => this.chunk(text).then((result) => {
|
|
3496
|
+
updateProgress();
|
|
3497
|
+
return result;
|
|
3498
|
+
})
|
|
3499
|
+
);
|
|
3500
|
+
const results = await Promise.all(chunkPromises);
|
|
3501
|
+
if (showProgress && total > 1 && completedCount > 0) {
|
|
3502
|
+
process.stdout.write("\n");
|
|
3503
|
+
}
|
|
3504
|
+
return results;
|
|
3505
|
+
}
|
|
3506
|
+
/**
|
|
3507
|
+
* Chunk a batch of texts, using either concurrent or sequential processing.
|
|
3508
|
+
*
|
|
3509
|
+
* If only one text is provided, processes it directly without batch overhead.
|
|
3510
|
+
*
|
|
3511
|
+
* @param {string[]} texts - The texts to chunk.
|
|
3512
|
+
* @param {boolean} [showProgress=true] - Whether to display progress in the console.
|
|
3513
|
+
* @returns {Promise<Chunk[][]>} An array of chunked results for each input text.
|
|
3514
|
+
*/
|
|
3515
|
+
async chunkBatch(texts, showProgress = true) {
|
|
3516
|
+
if (texts.length === 0) {
|
|
3517
|
+
return [];
|
|
3518
|
+
}
|
|
3519
|
+
if (texts.length === 1) {
|
|
3520
|
+
return [await this.chunk(texts[0])];
|
|
3521
|
+
}
|
|
3522
|
+
if (this._useConcurrency) {
|
|
3523
|
+
return this._concurrent_batch_processing(texts, showProgress);
|
|
3524
|
+
} else {
|
|
3525
|
+
return this._sequential_batch_processing(texts, showProgress);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
};
|
|
3529
|
+
|
|
3530
|
+
// src/chunking/sentence.ts
|
|
3531
|
+
var SentenceChunker = class _SentenceChunker extends BaseChunker {
|
|
3532
|
+
chunkSize;
|
|
3533
|
+
chunkOverlap;
|
|
3534
|
+
minSentencesPerChunk;
|
|
3535
|
+
minCharactersPerSentence;
|
|
3536
|
+
approximate;
|
|
3537
|
+
delim;
|
|
3538
|
+
includeDelim;
|
|
3539
|
+
sep;
|
|
3540
|
+
/**
|
|
3541
|
+
* Private constructor. Use `SentenceChunker.create()` to instantiate.
|
|
3542
|
+
*
|
|
3543
|
+
* @param {Tokenizer} tokenizer - The tokenizer to use for token counting.
|
|
3544
|
+
* @param {number} chunkSize - Maximum number of tokens per chunk.
|
|
3545
|
+
* @param {number} chunkOverlap - Number of tokens to overlap between consecutive chunks.
|
|
3546
|
+
* @param {number} minSentencesPerChunk - Minimum number of sentences per chunk.
|
|
3547
|
+
* @param {number} minCharactersPerSentence - Minimum number of characters for a valid sentence.
|
|
3548
|
+
* @param {boolean} approximate - Whether to use approximate token counting.
|
|
3549
|
+
* @param {string[]} delim - List of sentence delimiters to use for splitting.
|
|
3550
|
+
* @param {('prev' | 'next' | null)} includeDelim - Whether to include the delimiter with the previous sentence ('prev'), next sentence ('next'), or exclude it (null).
|
|
3551
|
+
*/
|
|
3552
|
+
constructor(tokenizer, chunkSize, chunkOverlap, minSentencesPerChunk, minCharactersPerSentence, approximate, delim, includeDelim) {
|
|
3553
|
+
super(tokenizer);
|
|
3554
|
+
if (chunkSize <= 0) {
|
|
3555
|
+
throw new Error("chunkSize must be greater than 0");
|
|
3556
|
+
}
|
|
3557
|
+
if (chunkOverlap < 0) {
|
|
3558
|
+
throw new Error("chunkOverlap must be non-negative");
|
|
3559
|
+
}
|
|
3560
|
+
if (chunkOverlap >= chunkSize) {
|
|
3561
|
+
throw new Error("chunkOverlap must be less than chunkSize");
|
|
3562
|
+
}
|
|
3563
|
+
if (minSentencesPerChunk <= 0) {
|
|
3564
|
+
throw new Error("minSentencesPerChunk must be greater than 0");
|
|
3565
|
+
}
|
|
3566
|
+
if (minCharactersPerSentence <= 0) {
|
|
3567
|
+
throw new Error("minCharactersPerSentence must be greater than 0");
|
|
3568
|
+
}
|
|
3569
|
+
if (!delim) {
|
|
3570
|
+
throw new Error("delim must be a list of strings or a string");
|
|
3571
|
+
}
|
|
3572
|
+
if (includeDelim !== "prev" && includeDelim !== "next" && includeDelim !== null) {
|
|
3573
|
+
throw new Error("includeDelim must be 'prev', 'next' or null");
|
|
3574
|
+
}
|
|
3575
|
+
if (approximate) {
|
|
3576
|
+
console.warn(
|
|
3577
|
+
"Approximate has been deprecated and will be removed from next version onwards!"
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
this.chunkSize = chunkSize;
|
|
3581
|
+
this.chunkOverlap = chunkOverlap;
|
|
3582
|
+
this.minSentencesPerChunk = minSentencesPerChunk;
|
|
3583
|
+
this.minCharactersPerSentence = minCharactersPerSentence;
|
|
3584
|
+
this.approximate = approximate;
|
|
3585
|
+
this.delim = delim;
|
|
3586
|
+
this.includeDelim = includeDelim;
|
|
3587
|
+
this.sep = "\u2704";
|
|
3588
|
+
}
|
|
3589
|
+
/**
|
|
3590
|
+
* Creates and initializes a SentenceChunker instance that is directly callable.
|
|
3591
|
+
*
|
|
3592
|
+
* This method is a static factory function that returns a Promise resolving to a CallableSentenceChunker instance.
|
|
3593
|
+
* The returned instance is a callable function that can be used to chunk text strings or arrays of text strings.
|
|
3594
|
+
*
|
|
3595
|
+
* @param {SentenceChunkerOptions} [options] - Options for configuring the SentenceChunker.
|
|
3596
|
+
* @returns {Promise<CallableSentenceChunker>} A promise that resolves to a callable SentenceChunker instance.
|
|
3597
|
+
*
|
|
3598
|
+
* @example
|
|
3599
|
+
* const chunker = await SentenceChunker.create();
|
|
3600
|
+
* const chunks = await chunker("This is a sample text.");
|
|
3601
|
+
* const batchChunks = await chunker(["Text 1", "Text 2"]);
|
|
3602
|
+
*
|
|
3603
|
+
* @see SentenceChunkerOptions
|
|
3604
|
+
*/
|
|
3605
|
+
static async create(options = {}) {
|
|
3606
|
+
const {
|
|
3607
|
+
tokenizer = "gpt-3.5-turbo",
|
|
3608
|
+
chunkSize = 512,
|
|
3609
|
+
chunkOverlap = 0,
|
|
3610
|
+
minSentencesPerChunk = 1,
|
|
3611
|
+
minCharactersPerSentence = 12,
|
|
3612
|
+
approximate = false,
|
|
3613
|
+
delim = [". ", "! ", "? ", "\n"],
|
|
3614
|
+
includeDelim = "prev"
|
|
3615
|
+
} = options;
|
|
3616
|
+
const tokenizerInstance = new ExuluTokenizer();
|
|
3617
|
+
await tokenizerInstance.create(tokenizer);
|
|
3618
|
+
const plainInstance = new _SentenceChunker(
|
|
3619
|
+
tokenizerInstance,
|
|
3620
|
+
chunkSize,
|
|
3621
|
+
chunkOverlap,
|
|
3622
|
+
minSentencesPerChunk,
|
|
3623
|
+
minCharactersPerSentence,
|
|
3624
|
+
approximate,
|
|
3625
|
+
delim,
|
|
3626
|
+
includeDelim
|
|
3627
|
+
);
|
|
3628
|
+
const callableFn = function(textOrTexts, showProgress) {
|
|
3629
|
+
if (typeof textOrTexts === "string") {
|
|
3630
|
+
return plainInstance.call(textOrTexts, showProgress);
|
|
3631
|
+
} else {
|
|
3632
|
+
return plainInstance.call(textOrTexts, showProgress);
|
|
3633
|
+
}
|
|
3634
|
+
};
|
|
3635
|
+
Object.setPrototypeOf(callableFn, _SentenceChunker.prototype);
|
|
3636
|
+
Object.assign(callableFn, plainInstance);
|
|
3637
|
+
return callableFn;
|
|
3638
|
+
}
|
|
3639
|
+
// NOTE: The replace + split method is not the best/most efficient way in general to be doing this. It works well in python because python implements .replace and .split in C while the re library is much slower in python.
|
|
3640
|
+
// NOTE: The new split -> join -> split is so weird, but it works. I don't quite like it however.
|
|
3641
|
+
// TODO: Implement a more efficient method for splitting text into sentences.
|
|
3642
|
+
/**
|
|
3643
|
+
* Fast sentence splitting while maintaining accuracy.
|
|
3644
|
+
*
|
|
3645
|
+
* @param {string} text - The text to split into sentences.
|
|
3646
|
+
* @returns {string[]} An array of sentences.
|
|
3647
|
+
*/
|
|
3648
|
+
_splitText(text) {
|
|
3649
|
+
let t = text;
|
|
3650
|
+
for (const c of this.delim) {
|
|
3651
|
+
if (this.includeDelim === "prev") {
|
|
3652
|
+
t = t.split(c).join(c + this.sep);
|
|
3653
|
+
} else if (this.includeDelim === "next") {
|
|
3654
|
+
t = t.split(c).join(this.sep + c);
|
|
3655
|
+
} else {
|
|
3656
|
+
t = t.split(c).join(this.sep);
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
const splits = t.split(this.sep);
|
|
3660
|
+
const sentences = [];
|
|
3661
|
+
let current = "";
|
|
3662
|
+
for (const s of splits) {
|
|
3663
|
+
if (!current) {
|
|
3664
|
+
current = s;
|
|
3665
|
+
} else {
|
|
3666
|
+
if (current.length >= this.minCharactersPerSentence) {
|
|
3667
|
+
sentences.push(current);
|
|
3668
|
+
current = s;
|
|
3669
|
+
} else {
|
|
3670
|
+
current += s;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
if (current) {
|
|
3675
|
+
sentences.push(current);
|
|
3676
|
+
}
|
|
3677
|
+
return sentences;
|
|
3678
|
+
}
|
|
3679
|
+
/**
|
|
3680
|
+
* Split text into sentences and calculate token counts for each sentence.
|
|
3681
|
+
*
|
|
3682
|
+
* @param {string} text - The text to split into sentences.
|
|
3683
|
+
* @returns {Promise<Sentence[]>} An array of Sentence objects.
|
|
3684
|
+
*/
|
|
3685
|
+
async _prepareSentences(text) {
|
|
3686
|
+
const sentenceTexts = this._splitText(text);
|
|
3687
|
+
if (!sentenceTexts.length) {
|
|
3688
|
+
return [];
|
|
3689
|
+
}
|
|
3690
|
+
const positions = [];
|
|
3691
|
+
let currentPos = 0;
|
|
3692
|
+
for (const sent of sentenceTexts) {
|
|
3693
|
+
positions.push(currentPos);
|
|
3694
|
+
currentPos += sent.length;
|
|
3695
|
+
}
|
|
3696
|
+
const tokenCounts = await this.tokenizer.countTokensBatch(sentenceTexts);
|
|
3697
|
+
return sentenceTexts.map(
|
|
3698
|
+
(sent, i) => new Sentence({
|
|
3699
|
+
text: sent,
|
|
3700
|
+
startIndex: positions[i],
|
|
3701
|
+
endIndex: positions[i] + sent.length,
|
|
3702
|
+
tokenCount: tokenCounts[i]
|
|
3703
|
+
})
|
|
3704
|
+
);
|
|
3705
|
+
}
|
|
3706
|
+
/**
|
|
3707
|
+
* Create a chunk from a list of sentences.
|
|
3708
|
+
*
|
|
3709
|
+
* @param {Sentence[]} sentences - The sentences to create a chunk from.
|
|
3710
|
+
* @returns {Promise<SentenceChunk>} A promise that resolves to a SentenceChunk object.
|
|
3711
|
+
*/
|
|
3712
|
+
async _createChunk(sentences) {
|
|
3713
|
+
const chunkText = sentences.map((sentence) => sentence.text).join("");
|
|
3714
|
+
const tokenCount = this.tokenizer.countTokens(chunkText);
|
|
3715
|
+
return new SentenceChunk({
|
|
3716
|
+
text: chunkText,
|
|
3717
|
+
startIndex: sentences[0].startIndex,
|
|
3718
|
+
endIndex: sentences[sentences.length - 1].endIndex,
|
|
3719
|
+
tokenCount,
|
|
3720
|
+
sentences
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
/**
|
|
3724
|
+
* Split text into overlapping chunks based on sentences while respecting token limits.
|
|
3725
|
+
*
|
|
3726
|
+
* @param {string} text - The text to split into chunks.
|
|
3727
|
+
* @returns {Promise<SentenceChunk[]>} A promise that resolves to an array of SentenceChunk objects.
|
|
3728
|
+
*/
|
|
3729
|
+
async chunk(text) {
|
|
3730
|
+
if (!text.trim()) {
|
|
3731
|
+
return [];
|
|
3732
|
+
}
|
|
3733
|
+
const sentences = await this._prepareSentences(text);
|
|
3734
|
+
if (!sentences.length) {
|
|
3735
|
+
return [];
|
|
3736
|
+
}
|
|
3737
|
+
const tokenSums = [];
|
|
3738
|
+
let sum = 0;
|
|
3739
|
+
for (const sentence of sentences) {
|
|
3740
|
+
tokenSums.push(sum);
|
|
3741
|
+
sum += sentence.tokenCount;
|
|
3742
|
+
}
|
|
3743
|
+
tokenSums.push(sum);
|
|
3744
|
+
const chunks = [];
|
|
3745
|
+
let pos = 0;
|
|
3746
|
+
while (pos < sentences.length) {
|
|
3747
|
+
const targetTokens = tokenSums[pos] + this.chunkSize;
|
|
3748
|
+
let splitIdx = this._bisectLeft(tokenSums, targetTokens, pos) - 1;
|
|
3749
|
+
splitIdx = Math.min(splitIdx, sentences.length);
|
|
3750
|
+
splitIdx = Math.max(splitIdx, pos + 1);
|
|
3751
|
+
if (splitIdx - pos < this.minSentencesPerChunk) {
|
|
3752
|
+
if (pos + this.minSentencesPerChunk <= sentences.length) {
|
|
3753
|
+
splitIdx = pos + this.minSentencesPerChunk;
|
|
3754
|
+
} else {
|
|
3755
|
+
console.warn(
|
|
3756
|
+
`Minimum sentences per chunk as ${this.minSentencesPerChunk} could not be met for all chunks. Last chunk of the text will have only ${sentences.length - pos} sentences. Consider increasing the chunk_size or decreasing the min_sentences_per_chunk.`
|
|
3757
|
+
);
|
|
3758
|
+
splitIdx = sentences.length;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
const chunkSentences = sentences.slice(pos, splitIdx);
|
|
3762
|
+
chunks.push(await this._createChunk(chunkSentences));
|
|
3763
|
+
if (this.chunkOverlap > 0 && splitIdx < sentences.length) {
|
|
3764
|
+
let overlapTokens = 0;
|
|
3765
|
+
let overlapIdx = splitIdx - 1;
|
|
3766
|
+
while (overlapIdx > pos && overlapTokens < this.chunkOverlap) {
|
|
3767
|
+
const sent = sentences[overlapIdx];
|
|
3768
|
+
const nextTokens = overlapTokens + sent.tokenCount + 1;
|
|
3769
|
+
if (nextTokens > this.chunkOverlap) {
|
|
3770
|
+
break;
|
|
3771
|
+
}
|
|
3772
|
+
overlapTokens = nextTokens;
|
|
3773
|
+
overlapIdx--;
|
|
3774
|
+
}
|
|
3775
|
+
pos = overlapIdx + 1;
|
|
3776
|
+
} else {
|
|
3777
|
+
pos = splitIdx;
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
await this.tokenizer.free();
|
|
3781
|
+
return chunks;
|
|
3782
|
+
}
|
|
3783
|
+
/**
|
|
3784
|
+
* Binary search to find the leftmost position where value should be inserted to maintain order.
|
|
3785
|
+
*
|
|
3786
|
+
* @param {number[]} arr - The array to search.
|
|
3787
|
+
* @param {number} value - The value to search for.
|
|
3788
|
+
* @param {number} [lo] - The starting index of the search.
|
|
3789
|
+
* @returns {number} The index of the leftmost position where value should be inserted.
|
|
3790
|
+
*/
|
|
3791
|
+
_bisectLeft(arr, value, lo = 0) {
|
|
3792
|
+
let hi = arr.length;
|
|
3793
|
+
while (lo < hi) {
|
|
3794
|
+
const mid = lo + hi >>> 1;
|
|
3795
|
+
if (arr[mid] < value) {
|
|
3796
|
+
lo = mid + 1;
|
|
3797
|
+
} else {
|
|
3798
|
+
hi = mid;
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
return lo;
|
|
3802
|
+
}
|
|
3803
|
+
/**
|
|
3804
|
+
* Return a string representation of the SentenceChunker.
|
|
3805
|
+
*
|
|
3806
|
+
* @returns {string} A string representation of the SentenceChunker.
|
|
3807
|
+
*/
|
|
3808
|
+
toString() {
|
|
3809
|
+
return `SentenceChunker(tokenizer=${JSON.stringify(this.tokenizer)}, chunkSize=${this.chunkSize}, chunkOverlap=${this.chunkOverlap}, minSentencesPerChunk=${this.minSentencesPerChunk}, minCharactersPerSentence=${this.minCharactersPerSentence}, approximate=${this.approximate}, delim=${JSON.stringify(this.delim)}, includeDelim=${this.includeDelim})`;
|
|
3810
|
+
}
|
|
3811
|
+
};
|
|
3812
|
+
|
|
3813
|
+
// src/exulu/chunker.ts
|
|
3814
|
+
var defaultChunker = async (item, maxChunkSize) => {
|
|
3815
|
+
const body = typeof item.content === "string" && item.content || typeof item.description === "string" && item.description || "";
|
|
3816
|
+
const name = typeof item.name === "string" ? item.name : "";
|
|
3817
|
+
const text = [name, body].filter(Boolean).join("\n\n").trim();
|
|
3818
|
+
if (!text) {
|
|
3819
|
+
return { item, chunks: [] };
|
|
3820
|
+
}
|
|
3821
|
+
const chunker = await SentenceChunker.create({ chunkSize: maxChunkSize });
|
|
3822
|
+
const sentenceChunks = await chunker(text);
|
|
3823
|
+
const chunks = sentenceChunks.map((c, index) => ({ content: c.text.trim(), index })).filter((c) => c.content.length > 0).map((c, index) => ({ content: c.content, index }));
|
|
3824
|
+
return { item, chunks };
|
|
3825
|
+
};
|
|
3826
|
+
|
|
3827
|
+
// src/exulu/litellm/parse-embedding-models.ts
|
|
3828
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
3829
|
+
import { resolve as resolve2 } from "path";
|
|
3830
|
+
var DEFAULT_MAX_CHUNK_SIZE = 1024;
|
|
3831
|
+
var DEFAULT_MAX_BATCH_SIZE = 100;
|
|
3832
|
+
var stripComment = (line) => {
|
|
3833
|
+
const idx = line.indexOf("#");
|
|
3834
|
+
return idx >= 0 ? line.slice(0, idx) : line;
|
|
3835
|
+
};
|
|
3836
|
+
var parseInt10 = (raw) => {
|
|
3837
|
+
const n = Number(raw.trim());
|
|
3838
|
+
return Number.isInteger(n) ? n : void 0;
|
|
3839
|
+
};
|
|
3840
|
+
var resolveLiteLLMConfigPath = () => process.env.LITELLM_CONFIG_PATH ?? resolve2(process.cwd(), "./config.litellm.yaml");
|
|
3841
|
+
var parseEmbeddingModels = (configPath) => {
|
|
3842
|
+
if (!existsSync2(configPath)) return [];
|
|
3843
|
+
const text = readFileSync(configPath, "utf8");
|
|
3844
|
+
const lines = text.split("\n");
|
|
3845
|
+
const entries = [];
|
|
3846
|
+
let current;
|
|
3847
|
+
for (const rawLine of lines) {
|
|
3848
|
+
const noComment = stripComment(rawLine);
|
|
3849
|
+
if (!noComment.trim()) continue;
|
|
3850
|
+
const indent = (rawLine.match(/^\s*/)?.[0] ?? "").length;
|
|
3851
|
+
const modelNameMatch = noComment.match(
|
|
3852
|
+
/^\s*-\s*model_name\s*:\s*["']?([^"'\s#]+)["']?\s*$/
|
|
3853
|
+
);
|
|
3854
|
+
if (modelNameMatch) {
|
|
3855
|
+
if (current) entries.push(current);
|
|
3856
|
+
current = { model_name: modelNameMatch[1], indent };
|
|
3857
|
+
continue;
|
|
3858
|
+
}
|
|
3859
|
+
if (!current) continue;
|
|
3860
|
+
if (indent <= current.indent && !/^\s*-\s/.test(rawLine)) {
|
|
3861
|
+
entries.push(current);
|
|
3862
|
+
current = void 0;
|
|
3863
|
+
continue;
|
|
3864
|
+
}
|
|
3865
|
+
const kvMatch = noComment.match(/^\s*(\w+)\s*:\s*(.+?)\s*$/);
|
|
3866
|
+
if (!kvMatch) continue;
|
|
3867
|
+
const key = kvMatch[1] ?? "";
|
|
3868
|
+
const rawValue = kvMatch[2] ?? "";
|
|
3869
|
+
switch (key) {
|
|
3870
|
+
case "dimensionality": {
|
|
3871
|
+
current.dimensionality = parseInt10(rawValue);
|
|
3872
|
+
break;
|
|
3873
|
+
}
|
|
3874
|
+
case "max_chunk_size": {
|
|
3875
|
+
current.max_chunk_size = parseInt10(rawValue);
|
|
3876
|
+
break;
|
|
3877
|
+
}
|
|
3878
|
+
case "max_batch_size": {
|
|
3879
|
+
current.max_batch_size = parseInt10(rawValue);
|
|
3880
|
+
break;
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
if (current) entries.push(current);
|
|
3885
|
+
return entries.filter((e) => typeof e.dimensionality === "number" && e.dimensionality > 0).map((e) => ({
|
|
3886
|
+
model_name: e.model_name,
|
|
3887
|
+
dimensionality: e.dimensionality,
|
|
3888
|
+
maxChunkSize: typeof e.max_chunk_size === "number" && e.max_chunk_size > 0 ? e.max_chunk_size : DEFAULT_MAX_CHUNK_SIZE,
|
|
3889
|
+
maxBatchSize: typeof e.max_batch_size === "number" && e.max_batch_size > 0 ? e.max_batch_size : DEFAULT_MAX_BATCH_SIZE
|
|
3890
|
+
}));
|
|
3891
|
+
};
|
|
3892
|
+
var getEmbeddingModelInfo = (modelName, configPath = resolveLiteLLMConfigPath()) => {
|
|
3893
|
+
const models2 = parseEmbeddingModels(configPath);
|
|
3894
|
+
const found = models2.find((m) => m.model_name === modelName);
|
|
3895
|
+
if (!found) {
|
|
3896
|
+
throw new Error(
|
|
3897
|
+
`[EXULU] Embedding model "${modelName}" was not found in ${configPath}, or its entry is missing a numeric \`model_info.dimensionality\`. Add it, e.g.:
|
|
3898
|
+
- model_name: ${modelName}
|
|
3899
|
+
litellm_params:
|
|
3900
|
+
model: <provider>/${modelName}
|
|
3901
|
+
model_info:
|
|
3902
|
+
dimensionality: 1024 # required (matches the model's output size)
|
|
3903
|
+
max_chunk_size: 1024 # optional
|
|
3904
|
+
max_batch_size: 100 # optional`
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
return found;
|
|
3908
|
+
};
|
|
3909
|
+
|
|
3910
|
+
// src/exulu/resolve-embedder.ts
|
|
3911
|
+
var ResolveEmbedderError = class extends Error {
|
|
3912
|
+
constructor(code, message) {
|
|
3913
|
+
super(message);
|
|
3914
|
+
this.code = code;
|
|
3915
|
+
this.name = "ResolveEmbedderError";
|
|
3916
|
+
}
|
|
3917
|
+
};
|
|
3918
|
+
async function resolveEmbedder(input) {
|
|
3919
|
+
const { model, contextId, contextName, user, userId, roleId, project, agent, routine } = input;
|
|
3920
|
+
if (!isLiteLLMEnabled()) {
|
|
3921
|
+
throw new ResolveEmbedderError(
|
|
3922
|
+
"LITELLM_NOT_CONFIGURED",
|
|
3923
|
+
"resolveEmbedder requires EXULU_USE_LITELLM=true \u2014 embeddings are served exclusively through the LiteLLM proxy."
|
|
3924
|
+
);
|
|
3925
|
+
}
|
|
3926
|
+
try {
|
|
3927
|
+
await waitForLiteLLMReady();
|
|
3928
|
+
} catch (err) {
|
|
3929
|
+
throw new ResolveEmbedderError(
|
|
3930
|
+
"LITELLM_NOT_READY",
|
|
3931
|
+
`LiteLLM is not ready: ${err.message}`
|
|
3932
|
+
);
|
|
3933
|
+
}
|
|
3934
|
+
const host = process.env.LITELLM_HOST ?? "127.0.0.1";
|
|
3935
|
+
const port = process.env.LITELLM_PORT ?? "4000";
|
|
3936
|
+
const masterKey = process.env.LITELLM_MASTER_KEY;
|
|
3937
|
+
if (!masterKey) {
|
|
3938
|
+
throw new ResolveEmbedderError(
|
|
3939
|
+
"LITELLM_NOT_CONFIGURED",
|
|
3940
|
+
"LITELLM_MASTER_KEY is required when EXULU_USE_LITELLM=true"
|
|
3941
|
+
);
|
|
3942
|
+
}
|
|
3943
|
+
const resolvedUserId = user?.id ?? userId;
|
|
3944
|
+
if (resolvedUserId) await provisionDefaultUserBudget(resolvedUserId);
|
|
3945
|
+
const { dimensionality, maxChunkSize, maxBatchSize } = getEmbeddingModelInfo(model);
|
|
3946
|
+
const role = user?.role;
|
|
3947
|
+
const tags = buildTags({
|
|
3948
|
+
user_id: resolvedUserId,
|
|
3949
|
+
role_id: role?.id ?? roleId,
|
|
3950
|
+
project_id: (project ?? user?.project)?.id,
|
|
3951
|
+
agent_id: agent?.id,
|
|
3952
|
+
team_id: user?.team?.id,
|
|
3953
|
+
routine_id: routine?.id,
|
|
3954
|
+
context_id: contextId,
|
|
3955
|
+
user_name: !user ? void 0 : user.type === "api" ? user.firstname ?? user.email : user.email,
|
|
3956
|
+
role_name: role?.name,
|
|
3957
|
+
project_name: (project ?? user?.project)?.name,
|
|
3958
|
+
agent_name: agent?.name,
|
|
3959
|
+
team_name: user?.team?.name,
|
|
3960
|
+
routine_name: routine?.name,
|
|
3961
|
+
context_name: contextName
|
|
3962
|
+
});
|
|
3963
|
+
const endpoint = `http://${host}:${port}/v1/embeddings`;
|
|
3964
|
+
const embedBatch = async (batch) => {
|
|
3965
|
+
const res = await fetch(endpoint, {
|
|
3966
|
+
method: "POST",
|
|
3967
|
+
headers: {
|
|
3968
|
+
Authorization: `Bearer ${masterKey}`,
|
|
3969
|
+
"Content-Type": "application/json"
|
|
3970
|
+
},
|
|
3971
|
+
body: JSON.stringify({
|
|
3972
|
+
model,
|
|
3973
|
+
input: batch,
|
|
3974
|
+
encoding_format: "float",
|
|
3975
|
+
// Pin the output size to the configured column dimensions. LiteLLM's
|
|
3976
|
+
// `drop_params: true` silently drops this for providers that don't
|
|
3977
|
+
// support it; for those, a dimensionality mismatch surfaces as an
|
|
3978
|
+
// insert error (the correct fail-fast).
|
|
3979
|
+
dimensions: dimensionality,
|
|
3980
|
+
// LiteLLM reads metadata.tags for tag-based spend tracking — same
|
|
3981
|
+
// mechanism createTaggedFetch uses for chat completions.
|
|
3982
|
+
metadata: { tags }
|
|
3983
|
+
})
|
|
3984
|
+
});
|
|
3985
|
+
if (!res.ok) {
|
|
3986
|
+
const text = await res.text().catch(() => "");
|
|
3987
|
+
throw new Error(
|
|
3988
|
+
`[EXULU] LiteLLM /v1/embeddings returned ${res.status} for model "${model}": ${text}`
|
|
3989
|
+
);
|
|
3990
|
+
}
|
|
3991
|
+
const json = await res.json();
|
|
3992
|
+
const data = json.data ?? [];
|
|
3993
|
+
const ordered = [...data].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
3994
|
+
const vectors = ordered.map((d) => d.embedding ?? []);
|
|
3995
|
+
if (vectors.length !== batch.length) {
|
|
3996
|
+
throw new Error(
|
|
3997
|
+
`[EXULU] LiteLLM /v1/embeddings returned ${vectors.length} vectors for ${batch.length} inputs (model "${model}").`
|
|
3998
|
+
);
|
|
3999
|
+
}
|
|
4000
|
+
return vectors;
|
|
4001
|
+
};
|
|
4002
|
+
const embed = async (inputs) => {
|
|
4003
|
+
if (inputs.length === 0) return [];
|
|
4004
|
+
const out = [];
|
|
4005
|
+
for (let i = 0; i < inputs.length; i += maxBatchSize) {
|
|
4006
|
+
const batch = inputs.slice(i, i + maxBatchSize);
|
|
4007
|
+
const vectors = await embedBatch(batch);
|
|
4008
|
+
out.push(...vectors);
|
|
4009
|
+
}
|
|
4010
|
+
return out;
|
|
4011
|
+
};
|
|
4012
|
+
return {
|
|
4013
|
+
model,
|
|
4014
|
+
dimensions: dimensionality,
|
|
4015
|
+
maxChunkSize,
|
|
4016
|
+
maxBatchSize,
|
|
4017
|
+
embed
|
|
4018
|
+
};
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
// types/enums/statistics.ts
|
|
4022
|
+
var STATISTICS_TYPE_ENUM = {
|
|
4023
|
+
CONTEXT_RETRIEVE: "CONTEXT_RETRIEVE",
|
|
4024
|
+
SOURCE_UPDATE: "SOURCE_UPDATE",
|
|
4025
|
+
EMBEDDER_UPSERT: "EMBEDDER_UPSERT",
|
|
4026
|
+
EMBEDDER_GENERATE: "EMBEDDER_GENERATE",
|
|
4027
|
+
EMBEDDER_DELETE: "EMBEDDER_DELETE",
|
|
4028
|
+
WORKFLOW_RUN: "WORKFLOW_RUN",
|
|
4029
|
+
CONTEXT_UPSERT: "CONTEXT_UPSERT",
|
|
4030
|
+
TOOL_CALL: "TOOL_CALL",
|
|
4031
|
+
AGENT_RUN: "AGENT_RUN"
|
|
4032
|
+
};
|
|
4033
|
+
|
|
2997
4034
|
// src/utils/query-preprocessing.ts
|
|
2998
4035
|
import { franc } from "franc";
|
|
2999
4036
|
import natural from "natural";
|
|
@@ -3266,19 +4303,6 @@ var applyFilters = (query, filters, table, field_prefix) => {
|
|
|
3266
4303
|
return query;
|
|
3267
4304
|
};
|
|
3268
4305
|
|
|
3269
|
-
// types/enums/statistics.ts
|
|
3270
|
-
var STATISTICS_TYPE_ENUM = {
|
|
3271
|
-
CONTEXT_RETRIEVE: "CONTEXT_RETRIEVE",
|
|
3272
|
-
SOURCE_UPDATE: "SOURCE_UPDATE",
|
|
3273
|
-
EMBEDDER_UPSERT: "EMBEDDER_UPSERT",
|
|
3274
|
-
EMBEDDER_GENERATE: "EMBEDDER_GENERATE",
|
|
3275
|
-
EMBEDDER_DELETE: "EMBEDDER_DELETE",
|
|
3276
|
-
WORKFLOW_RUN: "WORKFLOW_RUN",
|
|
3277
|
-
CONTEXT_UPSERT: "CONTEXT_UPSERT",
|
|
3278
|
-
TOOL_CALL: "TOOL_CALL",
|
|
3279
|
-
AGENT_RUN: "AGENT_RUN"
|
|
3280
|
-
};
|
|
3281
|
-
|
|
3282
4306
|
// types/models/vector-methods.ts
|
|
3283
4307
|
var VectorMethodEnum = {
|
|
3284
4308
|
"cosineDistance": "cosineDistance",
|
|
@@ -3939,6 +4963,11 @@ var agentsSchema = {
|
|
|
3939
4963
|
{
|
|
3940
4964
|
name: "animation_responding",
|
|
3941
4965
|
type: "text"
|
|
4966
|
+
},
|
|
4967
|
+
{
|
|
4968
|
+
name: "sandbox_enabled",
|
|
4969
|
+
type: "boolean",
|
|
4970
|
+
default: false
|
|
3942
4971
|
}
|
|
3943
4972
|
]
|
|
3944
4973
|
};
|
|
@@ -4027,6 +5056,15 @@ var usersSchema = {
|
|
|
4027
5056
|
name: "favourite_items",
|
|
4028
5057
|
type: "json"
|
|
4029
5058
|
},
|
|
5059
|
+
{
|
|
5060
|
+
// Knowledge: per-user "recently viewed" data items. Ordered JSON array
|
|
5061
|
+
// of global item ids ("<contextId>/<itemId>"), most-recent first, capped
|
|
5062
|
+
// client-side. Auto-added to existing DBs by the init-exulu-db column
|
|
5063
|
+
// sync; read via userById, written via usersUpdateOne (mirrors
|
|
5064
|
+
// favourite_items).
|
|
5065
|
+
name: "recently_viewed_items",
|
|
5066
|
+
type: "json"
|
|
5067
|
+
},
|
|
4030
5068
|
{
|
|
4031
5069
|
name: "firstname",
|
|
4032
5070
|
type: "text"
|
|
@@ -4106,6 +5144,12 @@ var usersSchema = {
|
|
|
4106
5144
|
{
|
|
4107
5145
|
name: "team",
|
|
4108
5146
|
type: "uuid"
|
|
5147
|
+
},
|
|
5148
|
+
{
|
|
5149
|
+
// Optional attribution target for API keys (type "api"): tags requests
|
|
5150
|
+
// triggered by the key with project_id_ for LiteLLM cost attribution.
|
|
5151
|
+
name: "project",
|
|
5152
|
+
type: "uuid"
|
|
4109
5153
|
}
|
|
4110
5154
|
]
|
|
4111
5155
|
};
|
|
@@ -4135,35 +5179,6 @@ var platformConfigurationsSchema = {
|
|
|
4135
5179
|
}
|
|
4136
5180
|
]
|
|
4137
5181
|
};
|
|
4138
|
-
var embedderSettingsSchema = {
|
|
4139
|
-
type: "embedder_settings",
|
|
4140
|
-
name: {
|
|
4141
|
-
plural: "embedder_settings",
|
|
4142
|
-
singular: "embedder_setting"
|
|
4143
|
-
},
|
|
4144
|
-
RBAC: false,
|
|
4145
|
-
fields: [
|
|
4146
|
-
{
|
|
4147
|
-
name: "context",
|
|
4148
|
-
type: "text"
|
|
4149
|
-
// id of the ExuluContext class
|
|
4150
|
-
},
|
|
4151
|
-
{
|
|
4152
|
-
name: "embedder",
|
|
4153
|
-
type: "text"
|
|
4154
|
-
// id of the ExuluEmbedder class
|
|
4155
|
-
},
|
|
4156
|
-
{
|
|
4157
|
-
name: "name",
|
|
4158
|
-
type: "text"
|
|
4159
|
-
},
|
|
4160
|
-
{
|
|
4161
|
-
name: "value",
|
|
4162
|
-
type: "text"
|
|
4163
|
-
// reference to an exulu variable
|
|
4164
|
-
}
|
|
4165
|
-
]
|
|
4166
|
-
};
|
|
4167
5182
|
var entityTypeSettingsSchema = {
|
|
4168
5183
|
type: "entity_type_settings",
|
|
4169
5184
|
name: {
|
|
@@ -4191,6 +5206,13 @@ var entityTypeSettingsSchema = {
|
|
|
4191
5206
|
name: "active",
|
|
4192
5207
|
type: "boolean",
|
|
4193
5208
|
default: true
|
|
5209
|
+
},
|
|
5210
|
+
{
|
|
5211
|
+
// "active" = a configured type (used in extraction); "suggested" = a type
|
|
5212
|
+
// the extractor proposed (active:false) awaiting promotion on the UI.
|
|
5213
|
+
name: "status",
|
|
5214
|
+
type: "text",
|
|
5215
|
+
default: "active"
|
|
4194
5216
|
}
|
|
4195
5217
|
]
|
|
4196
5218
|
};
|
|
@@ -4282,7 +5304,21 @@ var transcriptionJobsSchema = {
|
|
|
4282
5304
|
{ name: "target_rbac_users", type: "json" },
|
|
4283
5305
|
{ name: "target_rbac_roles", type: "json" },
|
|
4284
5306
|
{ name: "saved_item_id", type: "uuid", required: false },
|
|
4285
|
-
{ name: "error", type: "text" }
|
|
5307
|
+
{ name: "error", type: "text" },
|
|
5308
|
+
// Recall.ai meeting-bot fields. source discriminates the pipeline: whisper
|
|
5309
|
+
// rows are driven by the polling loop, recall rows by webhooks.
|
|
5310
|
+
// Design doc: docs/superpowers/specs/2026-06-19-recall-meeting-recording-design.md
|
|
5311
|
+
{ name: "source", type: "text", default: "whisper", index: true },
|
|
5312
|
+
{ name: "meeting_url", type: "text" },
|
|
5313
|
+
{ name: "recall_bot_id", type: "text", index: true },
|
|
5314
|
+
{ name: "recall_recording_id", type: "text", index: true },
|
|
5315
|
+
{ name: "recall_transcript_id", type: "text", index: true },
|
|
5316
|
+
{ name: "bot_status", type: "text" },
|
|
5317
|
+
{ name: "join_at", type: "date" },
|
|
5318
|
+
// Selected per-meeting post-processing: [{ prompt_id, agent_id }].
|
|
5319
|
+
{ name: "post_processing_prompts", type: "json" },
|
|
5320
|
+
// Results: [{ prompt_id, agent_id, prompt_name, status, output, error, ran_at }].
|
|
5321
|
+
{ name: "post_processing_outputs", type: "json" }
|
|
4286
5322
|
]
|
|
4287
5323
|
};
|
|
4288
5324
|
var imageGenerationsSchema = {
|
|
@@ -4337,6 +5373,26 @@ var oauthTokensSchema = {
|
|
|
4337
5373
|
// null = non-expiring
|
|
4338
5374
|
]
|
|
4339
5375
|
};
|
|
5376
|
+
var sharedArtifactsSchema = {
|
|
5377
|
+
type: "shared_artifacts",
|
|
5378
|
+
name: {
|
|
5379
|
+
plural: "shared_artifacts",
|
|
5380
|
+
singular: "shared_artifact"
|
|
5381
|
+
},
|
|
5382
|
+
// RBAC drives the "regular" auth_mode: rights_mode + the rbac table scope
|
|
5383
|
+
// who may view. public/password modes ignore rights_mode.
|
|
5384
|
+
RBAC: true,
|
|
5385
|
+
fields: [
|
|
5386
|
+
{ name: "name", type: "text", index: true, unique: true, required: true },
|
|
5387
|
+
{ name: "s3key", type: "text", required: true },
|
|
5388
|
+
{ name: "auth_mode", type: "text", default: "regular" },
|
|
5389
|
+
{ name: "password_hash", type: "text", required: false },
|
|
5390
|
+
// bcrypt; password mode only
|
|
5391
|
+
{ name: "expires_at", type: "date", required: false },
|
|
5392
|
+
// null = no expiry
|
|
5393
|
+
{ name: "content_type", type: "text", required: false }
|
|
5394
|
+
]
|
|
5395
|
+
};
|
|
4340
5396
|
var contextPresetsSchema = {
|
|
4341
5397
|
type: "context_presets",
|
|
4342
5398
|
name: {
|
|
@@ -4425,11 +5481,11 @@ var coreSchemas = {
|
|
|
4425
5481
|
variablesSchema: () => addCoreFields(variablesSchema),
|
|
4426
5482
|
platformConfigurationsSchema: () => addCoreFields(platformConfigurationsSchema),
|
|
4427
5483
|
promptLibrarySchema: () => addCoreFields(promptLibrarySchema),
|
|
4428
|
-
embedderSettingsSchema: () => addCoreFields(embedderSettingsSchema),
|
|
4429
5484
|
entityTypeSettingsSchema: () => addCoreFields(entityTypeSettingsSchema),
|
|
4430
5485
|
promptFavoritesSchema: () => addCoreFields(promptFavoritesSchema),
|
|
4431
5486
|
contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
|
|
4432
5487
|
oauthTokensSchema: () => addCoreFields(oauthTokensSchema),
|
|
5488
|
+
sharedArtifactsSchema: () => addCoreFields(sharedArtifactsSchema),
|
|
4433
5489
|
transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
|
|
4434
5490
|
imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
|
|
4435
5491
|
};
|
|
@@ -4560,11 +5616,89 @@ var entitiesEnabled = async (context) => {
|
|
|
4560
5616
|
const types = await hydrateEntityTypes(context);
|
|
4561
5617
|
return types.length > 0;
|
|
4562
5618
|
};
|
|
5619
|
+
var upsertEntitySuggestions = async (context, suggestions) => {
|
|
5620
|
+
if (!suggestions.length) return;
|
|
5621
|
+
try {
|
|
5622
|
+
const { db: db2 } = await postgresClient();
|
|
5623
|
+
const existing = await db2.from("entity_type_settings").where({ context: context.id }).select("name");
|
|
5624
|
+
const existingNames = new Set(
|
|
5625
|
+
existing.map(
|
|
5626
|
+
(r) => String(r.name || "").toLowerCase().trim()
|
|
5627
|
+
)
|
|
5628
|
+
);
|
|
5629
|
+
const rows = suggestions.filter((s) => s.name && !existingNames.has(s.name.toLowerCase().trim())).map((s) => ({
|
|
5630
|
+
name: s.name,
|
|
5631
|
+
description: s.example ? `${s.description} (e.g. ${s.example})` : s.description,
|
|
5632
|
+
context: context.id,
|
|
5633
|
+
active: false,
|
|
5634
|
+
status: "suggested"
|
|
5635
|
+
}));
|
|
5636
|
+
if (rows.length) {
|
|
5637
|
+
await db2.from("entity_type_settings").insert(rows);
|
|
5638
|
+
}
|
|
5639
|
+
} catch (err) {
|
|
5640
|
+
console.warn(
|
|
5641
|
+
"[EXULU] Could not persist entity suggestions:",
|
|
5642
|
+
err.message
|
|
5643
|
+
);
|
|
5644
|
+
}
|
|
5645
|
+
};
|
|
5646
|
+
var entityModelKey = (contextId) => `entity_extraction_model:${contextId}`;
|
|
5647
|
+
var getEntityModelSetting = async (contextId) => {
|
|
5648
|
+
try {
|
|
5649
|
+
const { db: db2 } = await postgresClient();
|
|
5650
|
+
const row = await db2.from("platform_configurations").where({ config_key: entityModelKey(contextId) }).first();
|
|
5651
|
+
if (!row?.config_value) return null;
|
|
5652
|
+
const raw = row.config_value;
|
|
5653
|
+
let value = raw;
|
|
5654
|
+
if (typeof raw === "string") {
|
|
5655
|
+
try {
|
|
5656
|
+
value = JSON.parse(raw);
|
|
5657
|
+
} catch {
|
|
5658
|
+
value = raw;
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
5662
|
+
} catch (err) {
|
|
5663
|
+
console.warn("[EXULU] Could not read entity model setting:", err.message);
|
|
5664
|
+
return null;
|
|
5665
|
+
}
|
|
5666
|
+
};
|
|
5667
|
+
var setEntityModelSetting = async (contextId, modelId) => {
|
|
5668
|
+
const { db: db2 } = await postgresClient();
|
|
5669
|
+
const key = entityModelKey(contextId);
|
|
5670
|
+
if (!modelId || !modelId.trim()) {
|
|
5671
|
+
await db2.from("platform_configurations").where({ config_key: key }).del();
|
|
5672
|
+
return;
|
|
5673
|
+
}
|
|
5674
|
+
const value = JSON.stringify(modelId.trim());
|
|
5675
|
+
await db2.from("platform_configurations").insert({
|
|
5676
|
+
config_key: key,
|
|
5677
|
+
config_value: value,
|
|
5678
|
+
description: `Entity extraction model for context ${contextId}`
|
|
5679
|
+
}).onConflict("config_key").merge({ config_value: value });
|
|
5680
|
+
};
|
|
5681
|
+
var resolveEntityModel = async (context) => {
|
|
5682
|
+
const databaseModel = await getEntityModelSetting(context.id);
|
|
5683
|
+
const codeModel = context.entities?.model ?? null;
|
|
5684
|
+
const envModel = process.env.EXULU_ENTITY_EXTRACTION_MODEL ?? null;
|
|
5685
|
+
if (databaseModel) {
|
|
5686
|
+
return { effectiveModel: databaseModel, source: "database", databaseModel, codeModel };
|
|
5687
|
+
}
|
|
5688
|
+
if (codeModel) {
|
|
5689
|
+
return { effectiveModel: codeModel, source: "code", databaseModel, codeModel };
|
|
5690
|
+
}
|
|
5691
|
+
if (envModel) {
|
|
5692
|
+
return { effectiveModel: envModel, source: "env", databaseModel, codeModel };
|
|
5693
|
+
}
|
|
5694
|
+
return { effectiveModel: null, source: null, databaseModel, codeModel };
|
|
5695
|
+
};
|
|
4563
5696
|
|
|
4564
5697
|
// src/exulu/entities/extractor.ts
|
|
4565
5698
|
import { generateText as generateText2, Output as Output2 } from "ai";
|
|
4566
5699
|
import { z as z5 } from "zod";
|
|
4567
5700
|
var CHUNK_BATCH_SIZE = 30;
|
|
5701
|
+
var MAX_SUGGESTIONS = 5;
|
|
4568
5702
|
var mentionSchema = z5.object({
|
|
4569
5703
|
entities: z5.array(
|
|
4570
5704
|
z5.object({
|
|
@@ -4574,23 +5708,51 @@ var mentionSchema = z5.object({
|
|
|
4574
5708
|
canonical: z5.string().describe("The canonical, language-normalized name used to merge variants."),
|
|
4575
5709
|
confidence: z5.number().min(0).max(1).describe("Confidence 0..1 that this is a valid entity.")
|
|
4576
5710
|
})
|
|
4577
|
-
)
|
|
5711
|
+
),
|
|
5712
|
+
suggestedTypes: z5.array(
|
|
5713
|
+
z5.object({
|
|
5714
|
+
name: z5.string().describe("A concise NEW entity type name, e.g. 'Error Code', 'Component'."),
|
|
5715
|
+
description: z5.string().describe("What this type captures, one sentence."),
|
|
5716
|
+
mentions: z5.array(
|
|
5717
|
+
z5.object({
|
|
5718
|
+
chunkIndex: z5.number().int().describe("Index of the chunk this mention was found in."),
|
|
5719
|
+
mention: z5.string().describe("The exact surface form as it appears in the text."),
|
|
5720
|
+
canonical: z5.string().describe("Canonical, language-normalized name."),
|
|
5721
|
+
confidence: z5.number().min(0).max(1).describe("Confidence 0..1.")
|
|
5722
|
+
})
|
|
5723
|
+
).describe("Every mention of this new type found in the provided chunks.")
|
|
5724
|
+
})
|
|
5725
|
+
).describe(
|
|
5726
|
+
"Entity TYPES that recur in the text but are NOT in the configured list and would be worth tracking. Empty array if none."
|
|
5727
|
+
).optional()
|
|
4578
5728
|
});
|
|
4579
5729
|
var buildSystemPrompt = (types, canonicalLanguage) => {
|
|
4580
5730
|
const typeList = types.map((t) => `- ${t.name}: ${t.description}`).join("\n");
|
|
5731
|
+
const typeNames = types.map((t) => `"${t.name}"`).join(", ");
|
|
4581
5732
|
return [
|
|
4582
|
-
"You are an entity extraction engine. Extract entities of ONLY the
|
|
5733
|
+
"You are an entity extraction engine. Extract entities of ONLY the types listed below from the provided text chunks.",
|
|
4583
5734
|
"",
|
|
4584
|
-
|
|
5735
|
+
'Entity types (the "type" field of every entity you output MUST be exactly one of these names):',
|
|
4585
5736
|
typeList,
|
|
4586
5737
|
"",
|
|
5738
|
+
"For each entity, output an object with:",
|
|
5739
|
+
`- "type": EXACTLY one of these type names \u2014 ${typeNames}. NEVER put the description, a category value, or a generic label like "named entity"/"classification" in this field.`,
|
|
5740
|
+
'- "mention" and "canonical": the value (see how to choose them below).',
|
|
5741
|
+
'- "chunkIndex": the index of the chunk the entity was found in.',
|
|
5742
|
+
'- "confidence": your certainty the entity is valid (0..1).',
|
|
5743
|
+
"",
|
|
5744
|
+
"How to choose mention/canonical depends on each type's description:",
|
|
5745
|
+
`- If the type names concrete things mentioned in the text (a person, product, place, code, organization): output the exact surface form as it appears as "mention", and a "canonical" normalized to ${canonicalLanguage}. Output a SEPARATE entity object for every distinct value \u2014 a single chunk may contain many of these, including several of the SAME type (e.g. three different cities \u2192 three "City" entities) and several of different types. Never collapse them or limit yourself to one per chunk.`,
|
|
5746
|
+
`- If the type is a classification or property (the description defines a fixed set of categories, or asks you to judge a property of the content \u2014 e.g. "is this a fact or an instruction"): pick the single best-fitting category for the chunk and output that category as BOTH "mention" and "canonical", even if that exact word is not in the text. Output at most one entity of such a type per chunk.`,
|
|
5747
|
+
"",
|
|
4587
5748
|
"Rules:",
|
|
4588
|
-
|
|
5749
|
+
"- A chunk can yield MULTIPLE entities \u2014 of the same named-entity type and of different types. Extract every distinct entity you find. The one-per-chunk limit applies ONLY to classification/property types.",
|
|
4589
5750
|
`- The canonical name merges variants and translations: e.g. "M\xFCnchen" and "MUC" both canonicalize to the ${canonicalLanguage} form "Munich". "Acme Inc" and "ACME" both canonicalize to "Acme".`,
|
|
4590
5751
|
"- DO NOT translate or alter identifiers, case numbers, SKUs, product codes, or proper product names \u2014 keep those verbatim as their own canonical.",
|
|
4591
|
-
"- Only
|
|
4592
|
-
"-
|
|
4593
|
-
"
|
|
5752
|
+
"- Only put entities of the listed types in the `entities` array. Do NOT put anything else there.",
|
|
5753
|
+
"- Use an empty `entities` array when no listed type applies to the text.",
|
|
5754
|
+
"",
|
|
5755
|
+
`Separately, in "suggestedTypes", propose up to ${MAX_SUGGESTIONS} entity TYPES that recur in the text but are NOT in the list above and would be worth tracking (e.g. a kind of code, component, or product that keeps appearing). For each, give a concise name, a one-sentence description, and the full list of its "mentions" found in the chunks \u2014 extract those mentions exactly as you would for a configured named-entity type (each with its chunkIndex, the surface-form "mention", a normalized "canonical", and "confidence"). Leave the array empty if nothing stands out. Never put these in the "entities" array.`
|
|
4594
5756
|
].join("\n");
|
|
4595
5757
|
};
|
|
4596
5758
|
var buildUserPrompt = (chunks) => {
|
|
@@ -4602,13 +5764,17 @@ var extractEntitiesForItem = async ({
|
|
|
4602
5764
|
chunks,
|
|
4603
5765
|
types
|
|
4604
5766
|
}) => {
|
|
4605
|
-
|
|
4606
|
-
|
|
5767
|
+
const empty = {
|
|
5768
|
+
mentions: [],
|
|
5769
|
+
suggestions: []
|
|
5770
|
+
};
|
|
5771
|
+
if (!types.length || !chunks.length) return empty;
|
|
5772
|
+
const { effectiveModel: modelId } = await resolveEntityModel(context);
|
|
4607
5773
|
if (!modelId) {
|
|
4608
5774
|
console.warn(
|
|
4609
|
-
`[EXULU] Entity extraction skipped for context ${context.id}: no entities.model
|
|
5775
|
+
`[EXULU] Entity extraction skipped for context ${context.id}: no model configured. Select one in the Entities tab, set context.entities.model in code, or set EXULU_ENTITY_EXTRACTION_MODEL.`
|
|
4610
5776
|
);
|
|
4611
|
-
return
|
|
5777
|
+
return empty;
|
|
4612
5778
|
}
|
|
4613
5779
|
const canonicalLanguage = context.entities?.canonicalLanguage || "english";
|
|
4614
5780
|
const confidenceThreshold = context.entities?.confidenceThreshold ?? 0.5;
|
|
@@ -4625,15 +5791,15 @@ var extractEntitiesForItem = async ({
|
|
|
4625
5791
|
`[EXULU] Entity extraction skipped for context ${context.id}: could not resolve model ${modelId}:`,
|
|
4626
5792
|
err.message
|
|
4627
5793
|
);
|
|
4628
|
-
return
|
|
5794
|
+
return empty;
|
|
4629
5795
|
}
|
|
4630
5796
|
const system = buildSystemPrompt(types, canonicalLanguage);
|
|
4631
|
-
const validTypeNames = new Set(types.map((t) => t.name.toLowerCase().trim()));
|
|
4632
5797
|
const batches = [];
|
|
4633
5798
|
for (let i = 0; i < chunks.length; i += CHUNK_BATCH_SIZE) {
|
|
4634
5799
|
batches.push(chunks.slice(i, i + CHUNK_BATCH_SIZE));
|
|
4635
5800
|
}
|
|
4636
5801
|
const mentions = [];
|
|
5802
|
+
const suggestionsByName = /* @__PURE__ */ new Map();
|
|
4637
5803
|
for (const batch of batches) {
|
|
4638
5804
|
try {
|
|
4639
5805
|
const { output } = await generateText2({
|
|
@@ -4647,16 +5813,47 @@ var extractEntitiesForItem = async ({
|
|
|
4647
5813
|
for (const e of output.entities) {
|
|
4648
5814
|
if (!e.mention || !e.canonical || !e.type) continue;
|
|
4649
5815
|
if (e.confidence < confidenceThreshold) continue;
|
|
4650
|
-
|
|
4651
|
-
const declared = types.find(
|
|
5816
|
+
const eType = e.type.toLowerCase().trim();
|
|
5817
|
+
const declared = types.find(
|
|
5818
|
+
(t) => t.name.toLowerCase().trim() === eType || (t.description || "").toLowerCase().trim() === eType
|
|
5819
|
+
);
|
|
5820
|
+
if (!declared) continue;
|
|
4652
5821
|
mentions.push({
|
|
4653
5822
|
chunkIndex: e.chunkIndex,
|
|
4654
|
-
type: declared
|
|
5823
|
+
type: declared.name,
|
|
4655
5824
|
mention: e.mention,
|
|
4656
5825
|
canonical: e.canonical,
|
|
4657
5826
|
confidence: e.confidence
|
|
4658
5827
|
});
|
|
4659
5828
|
}
|
|
5829
|
+
for (const s of output.suggestedTypes ?? []) {
|
|
5830
|
+
const name = (s?.name || "").trim();
|
|
5831
|
+
if (!name) continue;
|
|
5832
|
+
const key = name.toLowerCase();
|
|
5833
|
+
const isConfigured = types.some(
|
|
5834
|
+
(t) => t.name.toLowerCase().trim() === key || (t.description || "").toLowerCase().trim() === key
|
|
5835
|
+
);
|
|
5836
|
+
if (isConfigured) continue;
|
|
5837
|
+
const sMentions = (s.mentions ?? []).filter(
|
|
5838
|
+
(m) => m.mention && m.canonical && m.confidence >= confidenceThreshold
|
|
5839
|
+
);
|
|
5840
|
+
for (const m of sMentions) {
|
|
5841
|
+
mentions.push({
|
|
5842
|
+
chunkIndex: m.chunkIndex,
|
|
5843
|
+
type: name,
|
|
5844
|
+
mention: m.mention,
|
|
5845
|
+
canonical: m.canonical,
|
|
5846
|
+
confidence: m.confidence
|
|
5847
|
+
});
|
|
5848
|
+
}
|
|
5849
|
+
if (!suggestionsByName.has(key)) {
|
|
5850
|
+
suggestionsByName.set(key, {
|
|
5851
|
+
name,
|
|
5852
|
+
description: (s.description || "").trim(),
|
|
5853
|
+
example: sMentions[0]?.mention || void 0
|
|
5854
|
+
});
|
|
5855
|
+
}
|
|
5856
|
+
}
|
|
4660
5857
|
} catch (err) {
|
|
4661
5858
|
console.error(
|
|
4662
5859
|
`[EXULU] Entity extraction batch failed for context ${context.id} (continuing):`,
|
|
@@ -4664,7 +5861,11 @@ var extractEntitiesForItem = async ({
|
|
|
4664
5861
|
);
|
|
4665
5862
|
}
|
|
4666
5863
|
}
|
|
4667
|
-
|
|
5864
|
+
const suggestions = [...suggestionsByName.values()];
|
|
5865
|
+
console.log(
|
|
5866
|
+
`[EXULU][entities] context ${context.id}: kept ${mentions.length} mention(s), ${suggestions.length} suggestion(s).`
|
|
5867
|
+
);
|
|
5868
|
+
return { mentions, suggestions };
|
|
4668
5869
|
};
|
|
4669
5870
|
|
|
4670
5871
|
// src/exulu/entities/normalize.ts
|
|
@@ -4746,6 +5947,27 @@ var getEntityIdsForItem = async (context, itemId) => {
|
|
|
4746
5947
|
const rows = await db2(junctionTable).where({ item_id: itemId }).distinct("entity_id");
|
|
4747
5948
|
return rows.map((r) => r.entity_id);
|
|
4748
5949
|
};
|
|
5950
|
+
var getEntitiesForItem = async (context, itemId) => {
|
|
5951
|
+
if (!await chunkEntitiesTableExists(context)) return [];
|
|
5952
|
+
const { db: db2 } = await postgresClient();
|
|
5953
|
+
const junctionTable = getChunkEntitiesTableName(context.id);
|
|
5954
|
+
const entitiesTable = getEntitiesTableName(context.id);
|
|
5955
|
+
const rows = await db2(`${junctionTable} as j`).join(`${entitiesTable} as e`, "e.id", "j.entity_id").where("j.item_id", itemId).groupBy("e.id", "e.type", "e.display_name").select(
|
|
5956
|
+
"e.id as id",
|
|
5957
|
+
"e.type as type",
|
|
5958
|
+
"e.display_name as name",
|
|
5959
|
+
db2.raw("COUNT(*)::int as mentions")
|
|
5960
|
+
).orderBy([
|
|
5961
|
+
{ column: "e.type", order: "asc" },
|
|
5962
|
+
{ column: "e.display_name", order: "asc" }
|
|
5963
|
+
]);
|
|
5964
|
+
return rows.map((r) => ({
|
|
5965
|
+
id: r.id,
|
|
5966
|
+
type: r.type,
|
|
5967
|
+
name: r.name,
|
|
5968
|
+
mentions: Number(r.mentions) || 0
|
|
5969
|
+
}));
|
|
5970
|
+
};
|
|
4749
5971
|
var ingestEntitiesForItem = async ({
|
|
4750
5972
|
context,
|
|
4751
5973
|
itemId,
|
|
@@ -4830,6 +6052,37 @@ var ingestEntitiesForItem = async ({
|
|
|
4830
6052
|
});
|
|
4831
6053
|
});
|
|
4832
6054
|
};
|
|
6055
|
+
var detachEntitiesForItem = async (context, itemId) => {
|
|
6056
|
+
if (!await chunkEntitiesTableExists(context)) return 0;
|
|
6057
|
+
const { db: db2 } = await postgresClient();
|
|
6058
|
+
const entitiesTable = getEntitiesTableName(context.id);
|
|
6059
|
+
const junctionTable = getChunkEntitiesTableName(context.id);
|
|
6060
|
+
const itemsTable = getTableName(context.id);
|
|
6061
|
+
let detached = 0;
|
|
6062
|
+
await db2.transaction(async (trx) => {
|
|
6063
|
+
const affected = (await trx(junctionTable).where({ item_id: itemId }).distinct("entity_id")).map((r) => r.entity_id);
|
|
6064
|
+
detached = affected.length;
|
|
6065
|
+
if (!affected.length) return;
|
|
6066
|
+
await trx(junctionTable).where({ item_id: itemId }).delete();
|
|
6067
|
+
await trx(entitiesTable).whereIn("id", affected).update({ mention_count: 0, doc_count: 0 });
|
|
6068
|
+
const placeholders = affected.map(() => "?").join(",");
|
|
6069
|
+
await trx.raw(
|
|
6070
|
+
`UPDATE ${entitiesTable} e
|
|
6071
|
+
SET mention_count = sub.mc, doc_count = sub.dc
|
|
6072
|
+
FROM (
|
|
6073
|
+
SELECT entity_id, COUNT(*)::int AS mc, COUNT(DISTINCT item_id)::int AS dc
|
|
6074
|
+
FROM ${junctionTable}
|
|
6075
|
+
WHERE entity_id IN (${placeholders})
|
|
6076
|
+
GROUP BY entity_id
|
|
6077
|
+
) sub
|
|
6078
|
+
WHERE e.id = sub.entity_id`,
|
|
6079
|
+
affected
|
|
6080
|
+
);
|
|
6081
|
+
await trx(entitiesTable).whereIn("id", affected).where({ mention_count: 0 }).delete();
|
|
6082
|
+
await trx(itemsTable).where({ id: itemId }).update({ entities_updated_at: (/* @__PURE__ */ new Date()).toISOString(), entity_types_signature: null });
|
|
6083
|
+
});
|
|
6084
|
+
return detached;
|
|
6085
|
+
};
|
|
4833
6086
|
var resolveQueryEntities = async (context, mentions) => {
|
|
4834
6087
|
if (!mentions.length) return [];
|
|
4835
6088
|
const { db: db2 } = await postgresClient();
|
|
@@ -4961,22 +6214,31 @@ var extractAndIngestEntities = async ({
|
|
|
4961
6214
|
}) => {
|
|
4962
6215
|
try {
|
|
4963
6216
|
const types = await hydrateEntityTypes(context);
|
|
4964
|
-
if (!types.length || !context.embedder) return;
|
|
6217
|
+
if (!types.length || !context.embedder) return 0;
|
|
4965
6218
|
await ensureEntityTables(context);
|
|
4966
6219
|
const { db: db2 } = await postgresClient();
|
|
4967
6220
|
const chunkRows = await db2(getChunksTableName(context.id)).where({ source: itemId }).select("chunk_index", "content").orderBy("chunk_index", "asc");
|
|
4968
6221
|
const chunks = chunkRows.map((c) => ({ index: Number(c.chunk_index), content: c.content })).filter((c) => c.content);
|
|
4969
|
-
const mentions = await extractEntitiesForItem({
|
|
6222
|
+
const { mentions, suggestions } = await extractEntitiesForItem({
|
|
6223
|
+
context,
|
|
6224
|
+
chunks,
|
|
6225
|
+
types
|
|
6226
|
+
});
|
|
4970
6227
|
const signature = computeTypesSignature(types);
|
|
4971
6228
|
await ingestEntitiesForItem({ context, itemId, mentions, signature, previousEntityIds });
|
|
6229
|
+
if (suggestions.length) {
|
|
6230
|
+
await upsertEntitySuggestions(context, suggestions);
|
|
6231
|
+
}
|
|
4972
6232
|
console.log(
|
|
4973
6233
|
`[EXULU] Entity ingestion complete for item ${itemId} in context ${context.id}: ${mentions.length} mentions.`
|
|
4974
6234
|
);
|
|
6235
|
+
return mentions.length;
|
|
4975
6236
|
} catch (err) {
|
|
4976
6237
|
console.error(
|
|
4977
6238
|
`[EXULU] Entity ingestion failed for item ${itemId} in context ${context.id} (non-fatal):`,
|
|
4978
6239
|
err.message
|
|
4979
6240
|
);
|
|
6241
|
+
return 0;
|
|
4980
6242
|
}
|
|
4981
6243
|
};
|
|
4982
6244
|
|
|
@@ -4997,7 +6259,8 @@ var vectorSearch = async ({
|
|
|
4997
6259
|
trigger,
|
|
4998
6260
|
cutoffs,
|
|
4999
6261
|
expand,
|
|
5000
|
-
entityFilter
|
|
6262
|
+
entityFilter,
|
|
6263
|
+
queryEmbedding
|
|
5001
6264
|
}) => {
|
|
5002
6265
|
const table = convertContextToTableDefinition(context);
|
|
5003
6266
|
console.log("[EXULU] Called vector search.", {
|
|
@@ -5079,29 +6342,53 @@ var vectorSearch = async ({
|
|
|
5079
6342
|
let vector = [];
|
|
5080
6343
|
let vectorStr = "";
|
|
5081
6344
|
let vectorExpr = "";
|
|
6345
|
+
const _tBody = Date.now();
|
|
6346
|
+
let _preMs = 0, _statMs = 0, _resolveMs = 0, _embedMs = 0;
|
|
6347
|
+
let _embedSource = "none";
|
|
5082
6348
|
if (query) {
|
|
6349
|
+
const _tp = Date.now();
|
|
5083
6350
|
const { processed: stemmedQuery } = preprocessQuery(query, {
|
|
5084
6351
|
enableStemming: true,
|
|
5085
6352
|
detectLanguage: true
|
|
5086
6353
|
});
|
|
6354
|
+
_preMs = Date.now() - _tp;
|
|
5087
6355
|
console.log("[EXULU] Stemmed query:", stemmedQuery);
|
|
5088
6356
|
if (stemmedQuery) {
|
|
5089
6357
|
query = stemmedQuery;
|
|
5090
6358
|
}
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
6359
|
+
if (queryEmbedding && queryEmbedding.length) {
|
|
6360
|
+
vector = queryEmbedding;
|
|
6361
|
+
_embedSource = "reused";
|
|
6362
|
+
} else {
|
|
6363
|
+
const _ts = Date.now();
|
|
6364
|
+
await updateStatistic({
|
|
6365
|
+
name: "count",
|
|
5095
6366
|
label: table.name.singular,
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
6367
|
+
type: STATISTICS_TYPE_ENUM.EMBEDDER_GENERATE,
|
|
6368
|
+
trigger,
|
|
6369
|
+
count: 1,
|
|
6370
|
+
user: user?.id,
|
|
6371
|
+
role
|
|
6372
|
+
});
|
|
6373
|
+
_statMs = Date.now() - _ts;
|
|
6374
|
+
const _tr = Date.now();
|
|
6375
|
+
const resolved = await resolveEmbedder({
|
|
6376
|
+
model: embedder.model,
|
|
6377
|
+
contextId: context.id,
|
|
6378
|
+
contextName: context.name,
|
|
6379
|
+
user,
|
|
6380
|
+
roleId: role
|
|
6381
|
+
});
|
|
6382
|
+
_resolveMs = Date.now() - _tr;
|
|
6383
|
+
const _te = Date.now();
|
|
6384
|
+
const [queryVector] = await resolved.embed([query], { inputType: "query" });
|
|
6385
|
+
_embedMs = Date.now() - _te;
|
|
6386
|
+
if (!queryVector?.length) {
|
|
6387
|
+
throw new Error("No vector generated for query.");
|
|
6388
|
+
}
|
|
6389
|
+
vector = queryVector;
|
|
6390
|
+
_embedSource = "computed";
|
|
5103
6391
|
}
|
|
5104
|
-
vector = result.chunks[0].vector;
|
|
5105
6392
|
vectorStr = `ARRAY[${vector.join(",")}]`;
|
|
5106
6393
|
vectorExpr = `${vectorStr}::vector`;
|
|
5107
6394
|
}
|
|
@@ -5128,6 +6415,7 @@ var vectorSearch = async ({
|
|
|
5128
6415
|
const languages = configuration.languages?.length ? configuration.languages : ["english"];
|
|
5129
6416
|
console.log("[EXULU] Vector search params:", { method, query, cutoffs, languages });
|
|
5130
6417
|
let resultChunks = [];
|
|
6418
|
+
const _tSql = Date.now();
|
|
5131
6419
|
switch (method) {
|
|
5132
6420
|
case "tsvector":
|
|
5133
6421
|
chunksQuery.limit(limit * 2);
|
|
@@ -5241,6 +6529,11 @@ var vectorSearch = async ({
|
|
|
5241
6529
|
]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
|
|
5242
6530
|
resultChunks = await hybridQuery;
|
|
5243
6531
|
}
|
|
6532
|
+
if (process.env.EXULU_VS_TIMING) {
|
|
6533
|
+
console.log(
|
|
6534
|
+
`[EXULU-VS] ctx=${context.id} method=${method} embed=${_embedSource} pre=${_preMs}ms stat=${_statMs}ms resolve=${_resolveMs}ms embedApi=${_embedMs}ms sql=${Date.now() - _tSql}ms total=${Date.now() - _tBody}ms`
|
|
6535
|
+
);
|
|
6536
|
+
}
|
|
5244
6537
|
console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
|
|
5245
6538
|
let results = resultChunks.map((chunk) => ({
|
|
5246
6539
|
chunk_content: chunk.content,
|
|
@@ -5289,7 +6582,7 @@ var vectorSearch = async ({
|
|
|
5289
6582
|
if (entitiesOn && rawQuery) {
|
|
5290
6583
|
try {
|
|
5291
6584
|
const types = await hydrateEntityTypes(context);
|
|
5292
|
-
const queryMentions = await extractEntitiesForItem({
|
|
6585
|
+
const { mentions: queryMentions } = await extractEntitiesForItem({
|
|
5293
6586
|
context,
|
|
5294
6587
|
chunks: [{ index: 0, content: rawQuery }],
|
|
5295
6588
|
types
|
|
@@ -5328,7 +6621,6 @@ var vectorSearch = async ({
|
|
|
5328
6621
|
{ length: expand.before },
|
|
5329
6622
|
(_, i) => chunk.chunk_index - expand.before + i
|
|
5330
6623
|
).filter((index) => index >= 0);
|
|
5331
|
-
console.log("[EXULU] Indices to fetch:", indicesToFetch);
|
|
5332
6624
|
await Promise.all(
|
|
5333
6625
|
indicesToFetch.map(async (index) => {
|
|
5334
6626
|
if (expandedMap.has(`${chunk.item_id}-${index}`)) {
|
|
@@ -5373,7 +6665,6 @@ var vectorSearch = async ({
|
|
|
5373
6665
|
{ length: expand.after },
|
|
5374
6666
|
(_, i) => chunk.chunk_index + i + 1
|
|
5375
6667
|
);
|
|
5376
|
-
console.log("[EXULU] Indices to fetch:", indicesToFetch);
|
|
5377
6668
|
await Promise.all(
|
|
5378
6669
|
indicesToFetch.map(async (index) => {
|
|
5379
6670
|
if (expandedMap.has(`${chunk.item_id}-${index}`)) {
|
|
@@ -5449,7 +6740,7 @@ var vectorSearch = async ({
|
|
|
5449
6740
|
context: {
|
|
5450
6741
|
name: table.name.singular,
|
|
5451
6742
|
id: table.id || "",
|
|
5452
|
-
embedder: embedder.
|
|
6743
|
+
embedder: embedder.model
|
|
5453
6744
|
},
|
|
5454
6745
|
chunks: results,
|
|
5455
6746
|
entityInsights
|
|
@@ -5681,12 +6972,6 @@ var bullmqDecorator = async ({
|
|
|
5681
6972
|
};
|
|
5682
6973
|
|
|
5683
6974
|
// src/exulu/context.ts
|
|
5684
|
-
var getTableName = (id) => {
|
|
5685
|
-
return sanitizeName(id) + "_items";
|
|
5686
|
-
};
|
|
5687
|
-
var getChunksTableName = (id) => {
|
|
5688
|
-
return sanitizeName(id) + "_chunks";
|
|
5689
|
-
};
|
|
5690
6975
|
var ExuluContext2 = class {
|
|
5691
6976
|
// Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
|
|
5692
6977
|
// underscores and be a max length of 80 characters and at least 5 characters long.
|
|
@@ -5698,6 +6983,12 @@ var ExuluContext2 = class {
|
|
|
5698
6983
|
processor;
|
|
5699
6984
|
description;
|
|
5700
6985
|
embedder;
|
|
6986
|
+
/**
|
|
6987
|
+
* Splits an item into embeddable chunks. Moved here from the removed
|
|
6988
|
+
* ExuluEmbedder. When omitted, the built-in `defaultChunker` (SentenceChunker)
|
|
6989
|
+
* is used so a context works from just an embedder model name.
|
|
6990
|
+
*/
|
|
6991
|
+
chunker;
|
|
5701
6992
|
queryRewriter;
|
|
5702
6993
|
resultReranker;
|
|
5703
6994
|
configuration;
|
|
@@ -5713,6 +7004,7 @@ var ExuluContext2 = class {
|
|
|
5713
7004
|
name,
|
|
5714
7005
|
description,
|
|
5715
7006
|
embedder,
|
|
7007
|
+
chunker,
|
|
5716
7008
|
processor,
|
|
5717
7009
|
active,
|
|
5718
7010
|
fields,
|
|
@@ -5744,6 +7036,7 @@ var ExuluContext2 = class {
|
|
|
5744
7036
|
};
|
|
5745
7037
|
this.description = description;
|
|
5746
7038
|
this.embedder = embedder;
|
|
7039
|
+
this.chunker = chunker;
|
|
5747
7040
|
this.active = active;
|
|
5748
7041
|
this.queryRewriter = queryRewriter;
|
|
5749
7042
|
this.resultReranker = resultReranker;
|
|
@@ -5904,20 +7197,40 @@ var ExuluContext2 = class {
|
|
|
5904
7197
|
throw new Error("Item id is required for generating embeddings.");
|
|
5905
7198
|
}
|
|
5906
7199
|
const { db: db2 } = await postgresClient();
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
7200
|
+
if (statistics) {
|
|
7201
|
+
await updateStatistic({
|
|
7202
|
+
name: "count",
|
|
7203
|
+
label: statistics.label,
|
|
7204
|
+
type: STATISTICS_TYPE_ENUM.EMBEDDER_GENERATE,
|
|
7205
|
+
trigger: statistics.trigger,
|
|
7206
|
+
count: 1,
|
|
7207
|
+
user,
|
|
7208
|
+
role
|
|
7209
|
+
});
|
|
7210
|
+
}
|
|
7211
|
+
const source = item.id;
|
|
7212
|
+
const resolved = await resolveEmbedder({
|
|
7213
|
+
model: this.embedder.model,
|
|
7214
|
+
contextId: this.id,
|
|
7215
|
+
contextName: this.name,
|
|
7216
|
+
userId: user,
|
|
7217
|
+
roleId: role
|
|
7218
|
+
});
|
|
7219
|
+
const chunkerFn = this.chunker ?? defaultChunker;
|
|
7220
|
+
const { chunks: produced } = await chunkerFn(
|
|
7221
|
+
{ ...item, id: item.id },
|
|
7222
|
+
resolved.maxChunkSize,
|
|
7223
|
+
{ storage: new ExuluStorage({ config }) }
|
|
5920
7224
|
);
|
|
7225
|
+
console.log("[EXULU] Generating embeddings.");
|
|
7226
|
+
const contents = produced.map((c) => c.content);
|
|
7227
|
+
const vectors = contents.length ? await resolved.embed(contents, { inputType: "document" }) : [];
|
|
7228
|
+
const chunks = produced.map((c, i) => ({
|
|
7229
|
+
content: c.content,
|
|
7230
|
+
index: c.index,
|
|
7231
|
+
metadata: c.metadata ?? {},
|
|
7232
|
+
vector: vectors[i] ?? []
|
|
7233
|
+
}));
|
|
5921
7234
|
const previousEntityIds = await captureEntitiesBeforeReembed(this, item.id);
|
|
5922
7235
|
await db2.from(getChunksTableName(this.id)).where({ source }).delete();
|
|
5923
7236
|
if (chunks?.length) {
|
|
@@ -6152,12 +7465,18 @@ var ExuluContext2 = class {
|
|
|
6152
7465
|
};
|
|
6153
7466
|
getItems = async ({
|
|
6154
7467
|
filters,
|
|
6155
|
-
fields
|
|
7468
|
+
fields,
|
|
7469
|
+
user,
|
|
7470
|
+
role
|
|
6156
7471
|
}) => {
|
|
6157
7472
|
const { db: db2 } = await postgresClient();
|
|
6158
|
-
let query = db2.from(getTableName(this.id)).select(fields || "*");
|
|
6159
7473
|
const tableDefinition = convertContextToTableDefinition(this);
|
|
7474
|
+
let query = db2.from(getTableName(this.id)).select(fields || "*");
|
|
6160
7475
|
query = applyFilters(query, filters || [], tableDefinition);
|
|
7476
|
+
if (user) {
|
|
7477
|
+
const acUser = role && (!user.role || user.role.id !== role) ? { ...user, role: { ...user.role ?? {}, id: role } } : user;
|
|
7478
|
+
query = applyAccessControl(tableDefinition, query, acUser);
|
|
7479
|
+
}
|
|
6161
7480
|
const items = await query;
|
|
6162
7481
|
return items;
|
|
6163
7482
|
};
|
|
@@ -6188,8 +7507,8 @@ var ExuluContext2 = class {
|
|
|
6188
7507
|
console.log("[EXULU] embedder is in queue mode, scheduling job.");
|
|
6189
7508
|
const job = await bullmqDecorator({
|
|
6190
7509
|
timeoutInSeconds: queue.timeoutInSeconds || 180,
|
|
6191
|
-
label: `${this.embedder.
|
|
6192
|
-
embedder: this.embedder.
|
|
7510
|
+
label: `${this.embedder.model}`,
|
|
7511
|
+
embedder: this.embedder.model,
|
|
6193
7512
|
context: this.id,
|
|
6194
7513
|
backoff: queue.backoff || {
|
|
6195
7514
|
type: "exponential",
|
|
@@ -6214,7 +7533,7 @@ var ExuluContext2 = class {
|
|
|
6214
7533
|
config,
|
|
6215
7534
|
user,
|
|
6216
7535
|
{
|
|
6217
|
-
label: this.embedder.
|
|
7536
|
+
label: this.embedder.model,
|
|
6218
7537
|
trigger: trigger || "agent"
|
|
6219
7538
|
},
|
|
6220
7539
|
role,
|
|
@@ -6320,6 +7639,25 @@ var ExuluContext2 = class {
|
|
|
6320
7639
|
}
|
|
6321
7640
|
return { processed: batch.length, skipped };
|
|
6322
7641
|
},
|
|
7642
|
+
/**
|
|
7643
|
+
* Extract + ingest entities for a SINGLE item — powers the item detail
|
|
7644
|
+
* page's "Extract entities" test action. Returns the number of mentions
|
|
7645
|
+
* found so the UI can report the result.
|
|
7646
|
+
*/
|
|
7647
|
+
extractItem: async (itemId) => {
|
|
7648
|
+
if (!await entitiesEnabled(this)) {
|
|
7649
|
+
throw new Error(
|
|
7650
|
+
"Entity extraction is not configured for this context (no entity types, or no embedder)."
|
|
7651
|
+
);
|
|
7652
|
+
}
|
|
7653
|
+
const extracted = await extractAndIngestEntities({ context: this, itemId });
|
|
7654
|
+
return { extracted };
|
|
7655
|
+
},
|
|
7656
|
+
/** Detach all entities from a single item (drops links, prunes orphans). */
|
|
7657
|
+
detachItem: async (itemId) => {
|
|
7658
|
+
const detached = await detachEntitiesForItem(this, itemId);
|
|
7659
|
+
return { detached };
|
|
7660
|
+
},
|
|
6323
7661
|
/** Remove all entities (and their mentions via cascade) of a given type. */
|
|
6324
7662
|
purgeType: async (typeName) => {
|
|
6325
7663
|
if (!await entitiesTableExists(this)) return { removed: 0 };
|
|
@@ -6374,18 +7712,19 @@ var ExuluContext2 = class {
|
|
|
6374
7712
|
const { db: db2 } = await postgresClient();
|
|
6375
7713
|
const tableName = getChunksTableName(this.id);
|
|
6376
7714
|
console.log("[EXULU] Creating table: " + tableName);
|
|
7715
|
+
if (!this.embedder) {
|
|
7716
|
+
throw new Error(
|
|
7717
|
+
"Embedder must be set for context " + this.name + " to create chunks table."
|
|
7718
|
+
);
|
|
7719
|
+
}
|
|
7720
|
+
const { dimensionality } = getEmbeddingModelInfo(this.embedder.model);
|
|
6377
7721
|
await db2.schema.createTable(tableName, (table) => {
|
|
6378
|
-
if (!this.embedder) {
|
|
6379
|
-
throw new Error(
|
|
6380
|
-
"Embedder must be set for context " + this.name + " to create chunks table."
|
|
6381
|
-
);
|
|
6382
|
-
}
|
|
6383
7722
|
table.uuid("id").primary().defaultTo(db2.fn.uuid());
|
|
6384
7723
|
table.uuid("source").references("id").inTable(getTableName(this.id));
|
|
6385
7724
|
table.text("content");
|
|
6386
7725
|
table.jsonb("metadata");
|
|
6387
7726
|
table.integer("chunk_index");
|
|
6388
|
-
table.specificType("embedding", `vector(${
|
|
7727
|
+
table.specificType("embedding", `vector(${dimensionality})`);
|
|
6389
7728
|
const languages = this.configuration.languages?.length ? this.configuration.languages : ["english"];
|
|
6390
7729
|
const tsvectorExpression = languages.map((lang) => `to_tsvector('${lang}', coalesce(content, ''))`).join(" || ");
|
|
6391
7730
|
table.specificType(
|
|
@@ -7043,16 +8382,16 @@ async function createDynamicTools(chunks, hadExcludedContent) {
|
|
|
7043
8382
|
}
|
|
7044
8383
|
|
|
7045
8384
|
// ee/agentic-retrieval/v3/session-tools-registry.ts
|
|
7046
|
-
var
|
|
8385
|
+
var registry3 = /* @__PURE__ */ new Map();
|
|
7047
8386
|
function registerSessionTools(sessionId, tools) {
|
|
7048
|
-
const existing =
|
|
8387
|
+
const existing = registry3.get(sessionId) ?? /* @__PURE__ */ new Map();
|
|
7049
8388
|
for (const [name, toolDef] of Object.entries(tools)) {
|
|
7050
8389
|
existing.set(name, toolDef);
|
|
7051
8390
|
}
|
|
7052
|
-
|
|
8391
|
+
registry3.set(sessionId, existing);
|
|
7053
8392
|
}
|
|
7054
8393
|
function getSessionTools(sessionId) {
|
|
7055
|
-
const toolMap =
|
|
8394
|
+
const toolMap = registry3.get(sessionId);
|
|
7056
8395
|
if (!toolMap || toolMap.size === 0) return {};
|
|
7057
8396
|
return Object.fromEntries(toolMap.entries());
|
|
7058
8397
|
}
|
|
@@ -7162,8 +8501,8 @@ ${customInstructions}` : ""
|
|
|
7162
8501
|
(tc) => tc.toolName === "search_content" && tc.input?.includeContent === false || tc.toolName === "search_items_by_name"
|
|
7163
8502
|
);
|
|
7164
8503
|
if (reranker && stepChunks.length > 0) {
|
|
7165
|
-
console.log(`[EXULU] v3 reranking ${stepChunks.length} chunks with ${reranker.
|
|
7166
|
-
stepChunks = await reranker.
|
|
8504
|
+
console.log(`[EXULU] v3 reranking ${stepChunks.length} chunks with ${reranker.model}`);
|
|
8505
|
+
stepChunks = await reranker.rerank(query, stepChunks);
|
|
7167
8506
|
}
|
|
7168
8507
|
const newDynamic = await createDynamicTools(stepChunks, hadExcludedContent);
|
|
7169
8508
|
Object.assign(dynamicTools, newDynamic);
|
|
@@ -7251,7 +8590,7 @@ ${customInstructions}` : ""
|
|
|
7251
8590
|
}
|
|
7252
8591
|
|
|
7253
8592
|
// ee/agentic-retrieval/v3/trajectory.ts
|
|
7254
|
-
import * as
|
|
8593
|
+
import * as fs2 from "fs/promises";
|
|
7255
8594
|
import * as path from "path";
|
|
7256
8595
|
var trajectoryRegistry = {
|
|
7257
8596
|
lastFile: void 0
|
|
@@ -7452,13 +8791,13 @@ var TrajectoryLogger = class {
|
|
|
7452
8791
|
};
|
|
7453
8792
|
if (!writeFiles) return void 0;
|
|
7454
8793
|
try {
|
|
7455
|
-
await
|
|
8794
|
+
await fs2.mkdir(this.logDir, { recursive: true });
|
|
7456
8795
|
const ts = Date.now();
|
|
7457
8796
|
const jsonPath = path.join(this.logDir, `trajectory_${ts}.json`);
|
|
7458
8797
|
const mdPath = path.join(this.logDir, `trajectory_${ts}.md`);
|
|
7459
8798
|
await Promise.all([
|
|
7460
|
-
|
|
7461
|
-
|
|
8799
|
+
fs2.writeFile(jsonPath, JSON.stringify(this.data, null, 2), "utf-8"),
|
|
8800
|
+
fs2.writeFile(mdPath, this.toMarkdown(durationMs, success, error), "utf-8")
|
|
7462
8801
|
]);
|
|
7463
8802
|
console.log(`[EXULU] v3 trajectory saved: trajectory_${ts}.json + trajectory_${ts}.md`);
|
|
7464
8803
|
trajectoryRegistry.lastFile = jsonPath;
|
|
@@ -7626,7 +8965,6 @@ SCOPE CONSTRAINT: Retrieval is scoped to preselected items/contexts. Per context
|
|
|
7626
8965
|
function createAgenticRetrievalToolV3({
|
|
7627
8966
|
contexts,
|
|
7628
8967
|
instructions: adminInstructions,
|
|
7629
|
-
rerankers,
|
|
7630
8968
|
user,
|
|
7631
8969
|
role,
|
|
7632
8970
|
model,
|
|
@@ -7761,7 +9099,18 @@ function createAgenticRetrievalToolV3({
|
|
|
7761
9099
|
requiresPreselectedContexts = toolVariablesConfig["require_preselected_contexts"] === true || toolVariablesConfig["require_preselected_contexts"] === "true";
|
|
7762
9100
|
const rerankerId = toolVariablesConfig["reranker"];
|
|
7763
9101
|
if (rerankerId && rerankerId !== "none") {
|
|
7764
|
-
|
|
9102
|
+
try {
|
|
9103
|
+
configuredReranker = await resolveReranker({
|
|
9104
|
+
model: rerankerId,
|
|
9105
|
+
user,
|
|
9106
|
+
roleId: role
|
|
9107
|
+
});
|
|
9108
|
+
} catch (err) {
|
|
9109
|
+
console.warn(
|
|
9110
|
+
`[EXULU] v3 \u2014 could not resolve reranker "${rerankerId}", continuing without reranking:`,
|
|
9111
|
+
err
|
|
9112
|
+
);
|
|
9113
|
+
}
|
|
7765
9114
|
}
|
|
7766
9115
|
}
|
|
7767
9116
|
console.log("[EXULU] Managed context enabled:", managedContextEnabled);
|
|
@@ -7844,9 +9193,21 @@ var createNewMemoryItemTool = (agent, context) => {
|
|
|
7844
9193
|
case "longText":
|
|
7845
9194
|
case "shortText":
|
|
7846
9195
|
case "code":
|
|
7847
|
-
case "enum":
|
|
7848
9196
|
fields[field.name] = z10.string().describe("The " + field.name + " of the item to create");
|
|
7849
9197
|
break;
|
|
9198
|
+
case "enum":
|
|
9199
|
+
if (field.enumValues && field.enumValues.length > 0) {
|
|
9200
|
+
const enumValues = field.enumValues;
|
|
9201
|
+
fields[field.name] = z10.preprocess(
|
|
9202
|
+
(v) => typeof v === "string" ? v.toUpperCase() : v,
|
|
9203
|
+
z10.enum(enumValues)
|
|
9204
|
+
).describe(
|
|
9205
|
+
"The " + field.name + " of the item to create. Must be one of: " + field.enumValues.join(", ")
|
|
9206
|
+
);
|
|
9207
|
+
} else {
|
|
9208
|
+
fields[field.name] = z10.string().describe("The " + field.name + " of the item to create");
|
|
9209
|
+
}
|
|
9210
|
+
break;
|
|
7850
9211
|
case "json":
|
|
7851
9212
|
fields[field.name] = z10.string({}).describe(
|
|
7852
9213
|
"The " + field.name + " of the item to create, it should be a valid JSON string."
|
|
@@ -7872,6 +9233,9 @@ var createNewMemoryItemTool = (agent, context) => {
|
|
|
7872
9233
|
break;
|
|
7873
9234
|
}
|
|
7874
9235
|
}
|
|
9236
|
+
fields["visibility"] = z10.enum(["private", "public"]).optional().describe(
|
|
9237
|
+
"Whether this memory is private to the user or shared (public). Ask the user if unknown."
|
|
9238
|
+
);
|
|
7875
9239
|
const toolName = "create_" + sanitizeName(context.name) + "_memory_item";
|
|
7876
9240
|
return new ExuluTool({
|
|
7877
9241
|
id: toolName,
|
|
@@ -7881,14 +9245,36 @@ var createNewMemoryItemTool = (agent, context) => {
|
|
|
7881
9245
|
type: "function",
|
|
7882
9246
|
inputSchema: z10.object(fields),
|
|
7883
9247
|
config: [],
|
|
7884
|
-
execute: async (
|
|
9248
|
+
execute: async (params) => {
|
|
9249
|
+
const { name, description, surroundingContext, information, visibility, exuluConfig, user } = params;
|
|
7885
9250
|
let result = { result: "" };
|
|
9251
|
+
if (!visibility) {
|
|
9252
|
+
return {
|
|
9253
|
+
result: `Before saving this memory, ask the user whether it should be PRIVATE (visible only to them) or PUBLIC (shared with the team), then call \`${toolName}\` again with \`visibility\` set.`
|
|
9254
|
+
};
|
|
9255
|
+
}
|
|
7886
9256
|
try {
|
|
9257
|
+
const extraFields = {};
|
|
9258
|
+
for (const field of context.fields ?? []) {
|
|
9259
|
+
if (field.type === "enum" && field.enumValues && field.enumValues.length > 0) {
|
|
9260
|
+
const raw = params[field.name];
|
|
9261
|
+
if (raw !== void 0 && raw !== null && raw !== "") {
|
|
9262
|
+
const rawStr = String(raw);
|
|
9263
|
+
const canonical = field.enumValues.find(
|
|
9264
|
+
(v) => v.toUpperCase() === rawStr.toUpperCase()
|
|
9265
|
+
);
|
|
9266
|
+
if (canonical !== void 0) {
|
|
9267
|
+
extraFields[field.name] = canonical;
|
|
9268
|
+
}
|
|
9269
|
+
}
|
|
9270
|
+
}
|
|
9271
|
+
}
|
|
7887
9272
|
const newItem = {
|
|
7888
9273
|
name,
|
|
7889
9274
|
description: "Description: " + description + "\n\nSurrounding Context: " + surroundingContext,
|
|
7890
9275
|
information: "Information: " + information,
|
|
7891
|
-
rights_mode: "public"
|
|
9276
|
+
rights_mode: visibility === "private" ? "private" : "public",
|
|
9277
|
+
...extraFields
|
|
7892
9278
|
};
|
|
7893
9279
|
const { item: createdItem, job: createdJob } = await context.createItem(
|
|
7894
9280
|
newItem,
|
|
@@ -7926,14 +9312,14 @@ import {
|
|
|
7926
9312
|
SandboxManager
|
|
7927
9313
|
} from "@anthropic-ai/sandbox-runtime";
|
|
7928
9314
|
import { mkdir as mkdir2, rm, writeFile as writeFile2, readFile as fsReadFile, readdir, stat } from "fs/promises";
|
|
7929
|
-
import { existsSync as
|
|
7930
|
-
import { join as join3, dirname, resolve as
|
|
9315
|
+
import { existsSync as existsSync4 } from "fs";
|
|
9316
|
+
import { join as join3, dirname, resolve as resolve3, relative, posix } from "path";
|
|
7931
9317
|
import { exec as exec2, spawn as spawn2 } from "child_process";
|
|
7932
9318
|
import { promisify as promisify2 } from "util";
|
|
7933
9319
|
|
|
7934
9320
|
// src/exulu/system-dependencies.ts
|
|
7935
9321
|
import { exec } from "child_process";
|
|
7936
|
-
import { existsSync as
|
|
9322
|
+
import { existsSync as existsSync3 } from "fs";
|
|
7937
9323
|
import { join as join2 } from "path";
|
|
7938
9324
|
import { promisify } from "util";
|
|
7939
9325
|
var execAsync = promisify(exec);
|
|
@@ -7999,7 +9385,7 @@ async function probeDependency(dep) {
|
|
|
7999
9385
|
case "npm-global": {
|
|
8000
9386
|
const root = await getNpmGlobalRoot();
|
|
8001
9387
|
if (!root) return false;
|
|
8002
|
-
return
|
|
9388
|
+
return existsSync3(join2(root, dep.check.packageName));
|
|
8003
9389
|
}
|
|
8004
9390
|
}
|
|
8005
9391
|
}
|
|
@@ -8078,18 +9464,18 @@ function probeSandboxSupport() {
|
|
|
8078
9464
|
if (process.platform !== "linux") {
|
|
8079
9465
|
return { canSandbox: false, reason: `Unsupported platform: ${process.platform}` };
|
|
8080
9466
|
}
|
|
8081
|
-
return await new Promise((
|
|
9467
|
+
return await new Promise((resolve4) => {
|
|
8082
9468
|
const child = spawn2("bwrap", ["--dev-bind", "/", "/", "--", "/bin/true"]);
|
|
8083
9469
|
let stderr = "";
|
|
8084
9470
|
child.stderr.on("data", (chunk) => {
|
|
8085
9471
|
stderr += chunk.toString();
|
|
8086
9472
|
});
|
|
8087
9473
|
child.on("error", (err) => {
|
|
8088
|
-
|
|
9474
|
+
resolve4({ canSandbox: false, reason: `bwrap not executable: ${err.message}` });
|
|
8089
9475
|
});
|
|
8090
9476
|
child.on("exit", (code) => {
|
|
8091
|
-
if (code === 0)
|
|
8092
|
-
else
|
|
9477
|
+
if (code === 0) resolve4({ canSandbox: true });
|
|
9478
|
+
else resolve4({ canSandbox: false, reason: stderr.trim() || `bwrap exited ${code}` });
|
|
8093
9479
|
});
|
|
8094
9480
|
});
|
|
8095
9481
|
})();
|
|
@@ -8135,7 +9521,7 @@ async function downloadSkill(skill, skillsDirectory, config) {
|
|
|
8135
9521
|
}
|
|
8136
9522
|
}
|
|
8137
9523
|
function isArtifactPath(absPath, sessionDir) {
|
|
8138
|
-
const resolved =
|
|
9524
|
+
const resolved = resolve3(absPath);
|
|
8139
9525
|
const rel = relative(sessionDir, resolved);
|
|
8140
9526
|
if (!rel || rel.startsWith("..")) return false;
|
|
8141
9527
|
const first = rel.split("/")[0];
|
|
@@ -8194,7 +9580,7 @@ async function restoreArtifactsFromS3(sessionDir, sessionId, userId, config) {
|
|
|
8194
9580
|
async function downloadKeyIntoSandbox(opts) {
|
|
8195
9581
|
const { sessionId, userId, fullS3Key, config } = opts;
|
|
8196
9582
|
const sessionDir = join3("/tmp", "exulu-sessions", sessionId);
|
|
8197
|
-
if (!
|
|
9583
|
+
if (!existsSync4(sessionDir)) {
|
|
8198
9584
|
return { written: false };
|
|
8199
9585
|
}
|
|
8200
9586
|
const userPrefix = `user_${userId}/sessions/${sessionId}/`;
|
|
@@ -8228,7 +9614,7 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
8228
9614
|
return cached.handle;
|
|
8229
9615
|
}
|
|
8230
9616
|
const sessionDir = join3("/tmp", "exulu-sessions", sessionId);
|
|
8231
|
-
const dirExisted =
|
|
9617
|
+
const dirExisted = existsSync4(sessionDir);
|
|
8232
9618
|
await mkdir2(sessionDir, { recursive: true });
|
|
8233
9619
|
const skillsDirectory = join3(sessionDir, "skills");
|
|
8234
9620
|
const installedSkills = /* @__PURE__ */ new Map();
|
|
@@ -8355,7 +9741,7 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
8355
9741
|
if (!persistenceEnabled || !isArtifactPath(absPath, sessionDir)) {
|
|
8356
9742
|
return {};
|
|
8357
9743
|
}
|
|
8358
|
-
const rel = relative(sessionDir,
|
|
9744
|
+
const rel = relative(sessionDir, resolve3(absPath));
|
|
8359
9745
|
const s3Key = artifactS3Key(sessionId, rel);
|
|
8360
9746
|
const out = {};
|
|
8361
9747
|
try {
|
|
@@ -8647,7 +10033,7 @@ var hydrateVariables = async (tool6) => {
|
|
|
8647
10033
|
await Promise.all(promises);
|
|
8648
10034
|
return tool6;
|
|
8649
10035
|
};
|
|
8650
|
-
var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, providerapikey, contexts,
|
|
10036
|
+
var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, providerapikey, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems) => {
|
|
8651
10037
|
if (!currentTools) return {};
|
|
8652
10038
|
if (!allExuluTools) {
|
|
8653
10039
|
allExuluTools = [];
|
|
@@ -8656,7 +10042,7 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
8656
10042
|
contexts = [];
|
|
8657
10043
|
}
|
|
8658
10044
|
let sharedSessionSandbox;
|
|
8659
|
-
if (sessionID && exuluConfig) {
|
|
10045
|
+
if (sessionID && exuluConfig && agent?.sandbox_enabled === true) {
|
|
8660
10046
|
try {
|
|
8661
10047
|
sharedSessionSandbox = await createSessionSandbox(
|
|
8662
10048
|
sessionID,
|
|
@@ -8715,7 +10101,6 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
8715
10101
|
const agenticSearchTool = createAgenticRetrievalToolV3({
|
|
8716
10102
|
contexts: contexts.filter((context) => context.id !== agent?.memory),
|
|
8717
10103
|
// dont include the agents memory in the agentic search tool!
|
|
8718
|
-
rerankers: rerankers || [],
|
|
8719
10104
|
user,
|
|
8720
10105
|
role: user?.role?.id,
|
|
8721
10106
|
model,
|
|
@@ -8911,6 +10296,7 @@ export {
|
|
|
8911
10296
|
postgresClient,
|
|
8912
10297
|
authentication,
|
|
8913
10298
|
STATISTICS_TYPE_ENUM,
|
|
10299
|
+
resolveLiteLLMConfigPath,
|
|
8914
10300
|
getPresignedUrl,
|
|
8915
10301
|
uploadFile,
|
|
8916
10302
|
listS3ObjectsByPrefix,
|
|
@@ -8923,17 +10309,17 @@ export {
|
|
|
8923
10309
|
createUppyRoutes,
|
|
8924
10310
|
ExuluStorage,
|
|
8925
10311
|
sanitizeName,
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
checkRecordAccess,
|
|
10312
|
+
getTableName,
|
|
10313
|
+
getChunksTableName,
|
|
10314
|
+
ExuluTokenizer,
|
|
10315
|
+
Chunk,
|
|
10316
|
+
BaseChunker,
|
|
10317
|
+
SentenceChunker,
|
|
10318
|
+
defaultChunker,
|
|
8934
10319
|
LITELLM_UI_PATH,
|
|
8935
10320
|
isLiteLLMEnabled,
|
|
8936
10321
|
setLiteLLMPackageRoot,
|
|
10322
|
+
enableLiteLLMClientMode,
|
|
8937
10323
|
startLiteLLMSupervisor,
|
|
8938
10324
|
waitForLiteLLMReady,
|
|
8939
10325
|
buildTags,
|
|
@@ -8951,16 +10337,29 @@ export {
|
|
|
8951
10337
|
getTagBudgetMap,
|
|
8952
10338
|
provisionDefaultUserBudget,
|
|
8953
10339
|
getUserBudgetView,
|
|
10340
|
+
resolveEmbedder,
|
|
10341
|
+
updateStatistic,
|
|
10342
|
+
applySorting,
|
|
10343
|
+
applyAccessControl,
|
|
10344
|
+
applyFilters,
|
|
10345
|
+
checkLicense,
|
|
10346
|
+
coreSchemas,
|
|
10347
|
+
convertContextToTableDefinition,
|
|
10348
|
+
setEntityModelSetting,
|
|
10349
|
+
resolveEntityModel,
|
|
10350
|
+
checkRecordAccess,
|
|
8954
10351
|
ResolveModelError,
|
|
8955
10352
|
resolveModel,
|
|
8956
10353
|
exuluApp,
|
|
10354
|
+
getEntitiesTableName,
|
|
10355
|
+
getChunkEntitiesTableName,
|
|
10356
|
+
getEntitiesForItem,
|
|
8957
10357
|
ensureEntityTables,
|
|
8958
10358
|
vectorSearch,
|
|
8959
10359
|
mapType,
|
|
8960
10360
|
maybePruneJobResults,
|
|
8961
|
-
getTableName,
|
|
8962
|
-
getChunksTableName,
|
|
8963
10361
|
ExuluContext2 as ExuluContext,
|
|
10362
|
+
resolveReranker,
|
|
8964
10363
|
oauthRegistry,
|
|
8965
10364
|
oauthTokenStore,
|
|
8966
10365
|
OAUTH_CALLBACK_PATH,
|