@exulu/backend 1.68.0 → 1.69.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VPSLTGZF.js → chunk-PJSDLFXL.js} +167 -48
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-CHQF36XW.js → convert-exulu-tools-to-ai-sdk-tools-MXLGIOCT.js} +1 -1
- package/dist/index.cjs +592 -113
- package/dist/index.d.cts +60 -2
- package/dist/index.d.ts +60 -2
- package/dist/index.js +394 -52
- package/ee/python/documents/processing/doc_processor.ts +61 -10
- 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 +6 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,8 +43,10 @@ import {
|
|
|
43
43
|
exchangeCodeForTokens,
|
|
44
44
|
exuluApp,
|
|
45
45
|
getBudgetSettings,
|
|
46
|
+
getChunkEntitiesTableName,
|
|
46
47
|
getChunksTableName,
|
|
47
48
|
getEntitiesForItem,
|
|
49
|
+
getEntitiesTableName,
|
|
48
50
|
getPresignedUrl,
|
|
49
51
|
getS3ObjectBytes,
|
|
50
52
|
getS3ObjectContent,
|
|
@@ -66,6 +68,7 @@ import {
|
|
|
66
68
|
postgresClient,
|
|
67
69
|
provisionDefaultUserBudget,
|
|
68
70
|
reportSystemDependencies,
|
|
71
|
+
resolveEmbedder,
|
|
69
72
|
resolveEntityModel,
|
|
70
73
|
resolveLiteLLMConfigPath,
|
|
71
74
|
resolveModel,
|
|
@@ -85,7 +88,7 @@ import {
|
|
|
85
88
|
vectorSearch,
|
|
86
89
|
waitForLiteLLMReady,
|
|
87
90
|
withRetry
|
|
88
|
-
} from "./chunk-
|
|
91
|
+
} from "./chunk-PJSDLFXL.js";
|
|
89
92
|
import {
|
|
90
93
|
findLiteLLMModel
|
|
91
94
|
} from "./chunk-YCE44CMU.js";
|
|
@@ -104,6 +107,72 @@ var redisServer = {
|
|
|
104
107
|
username: process.env.REDIS_USER || ""
|
|
105
108
|
};
|
|
106
109
|
|
|
110
|
+
// ee/queues/redis-startup.ts
|
|
111
|
+
var REDIS_STARTUP_TIMEOUT_MS = 6e4;
|
|
112
|
+
var WATCHDOG_INTERVAL_MS = 1e4;
|
|
113
|
+
var ERROR_LOG_THROTTLE_MS = 3e4;
|
|
114
|
+
var log = (line) => console.log(`[EXULU-REDIS] ${line}`);
|
|
115
|
+
var warn = (line) => console.warn(`[EXULU-REDIS] ${line}`);
|
|
116
|
+
var errorLog = (line) => console.error(`[EXULU-REDIS] ${line}`);
|
|
117
|
+
var redisAddress = () => `${redisServer.host || "(unset)"}:${redisServer.port || "(unset)"}`;
|
|
118
|
+
var describeError = (e) => {
|
|
119
|
+
const any = e;
|
|
120
|
+
const head = any?.message ? String(any.message).split("\n")[0] : void 0;
|
|
121
|
+
if (any?.code) return head && head !== any.code ? `${any.code} (${head})` : `${any.code}`;
|
|
122
|
+
return head ?? String(e);
|
|
123
|
+
};
|
|
124
|
+
function logRedisErrors(source, label) {
|
|
125
|
+
let count = 0;
|
|
126
|
+
let lastLoggedAt = 0;
|
|
127
|
+
source.on("error", (err) => {
|
|
128
|
+
count += 1;
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
if (count === 1 || now - lastLoggedAt >= ERROR_LOG_THROTTLE_MS) {
|
|
131
|
+
errorLog(`${label} connection error (${redisAddress()}): ${describeError(err)}${count > 1 ? ` (x${count})` : ""}`);
|
|
132
|
+
lastLoggedAt = now;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function guardRedisStartup(label, run, source) {
|
|
137
|
+
const addr = redisAddress();
|
|
138
|
+
log(`Connecting to Redis (${addr}) for ${label}\u2026`);
|
|
139
|
+
const startedAt = Date.now();
|
|
140
|
+
let lastError;
|
|
141
|
+
const onError = (err) => {
|
|
142
|
+
lastError = err;
|
|
143
|
+
};
|
|
144
|
+
source?.on("error", onError);
|
|
145
|
+
const watchdog = setInterval(() => {
|
|
146
|
+
const secs = Math.round((Date.now() - startedAt) / 1e3);
|
|
147
|
+
warn(
|
|
148
|
+
`\u26A0 Still waiting for Redis at ${addr} after ${secs}s \u2014 ${label} startup is blocked. Is Redis running? (aborting at ${REDIS_STARTUP_TIMEOUT_MS / 1e3}s)`
|
|
149
|
+
);
|
|
150
|
+
}, WATCHDOG_INTERVAL_MS);
|
|
151
|
+
watchdog.unref?.();
|
|
152
|
+
let timer2;
|
|
153
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
154
|
+
timer2 = setTimeout(() => {
|
|
155
|
+
reject(
|
|
156
|
+
new Error(
|
|
157
|
+
`[EXULU-REDIS] Redis unreachable at ${addr} after ${REDIS_STARTUP_TIMEOUT_MS / 1e3}s \u2014 aborting ${label} startup. Last error: ${lastError ? describeError(lastError) : "none surfaced"}. Check REDIS_HOST/REDIS_PORT and that a Redis server is reachable at ${addr}.`
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
}, REDIS_STARTUP_TIMEOUT_MS);
|
|
161
|
+
});
|
|
162
|
+
const runPromise = Promise.resolve().then(run);
|
|
163
|
+
runPromise.catch(() => {
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
const result = await Promise.race([runPromise, timeout]);
|
|
167
|
+
log(`Redis ready; ${label} initialized (${addr}, ${((Date.now() - startedAt) / 1e3).toFixed(1)}s).`);
|
|
168
|
+
return result;
|
|
169
|
+
} finally {
|
|
170
|
+
clearInterval(watchdog);
|
|
171
|
+
if (timer2) clearTimeout(timer2);
|
|
172
|
+
source?.off?.("error", onError);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
107
176
|
// src/redis/client.ts
|
|
108
177
|
var client = {};
|
|
109
178
|
async function redisClient() {
|
|
@@ -121,9 +190,11 @@ async function redisClient() {
|
|
|
121
190
|
client["exulu"] = createClient({
|
|
122
191
|
url
|
|
123
192
|
});
|
|
124
|
-
|
|
193
|
+
logRedisErrors(client["exulu"], "client");
|
|
194
|
+
await guardRedisStartup("client", () => client["exulu"].connect().then(() => void 0), client["exulu"]);
|
|
125
195
|
} catch (error) {
|
|
126
196
|
console.error(`[EXULU] error connecting to redis:`, error);
|
|
197
|
+
delete client["exulu"];
|
|
127
198
|
return { client: null };
|
|
128
199
|
}
|
|
129
200
|
}
|
|
@@ -141,7 +212,8 @@ import "express";
|
|
|
141
212
|
// src/validators/requests.ts
|
|
142
213
|
var requestValidators = {
|
|
143
214
|
authenticate: async (req) => {
|
|
144
|
-
const
|
|
215
|
+
const rawApiKey = req.headers["exulu-api-key"] || req.headers["x-api-key"];
|
|
216
|
+
const apikey = rawApiKey?.replace(/^Bearer\s+/i, "") || null;
|
|
145
217
|
const { db } = await postgresClient();
|
|
146
218
|
let authtoken = null;
|
|
147
219
|
if (typeof apikey !== "string") {
|
|
@@ -405,6 +477,14 @@ var ExuluQueues = class {
|
|
|
405
477
|
},
|
|
406
478
|
telemetry: new BullMQOtel("simple-guide")
|
|
407
479
|
});
|
|
480
|
+
logRedisErrors(newQueue, `queue "${name}"`);
|
|
481
|
+
try {
|
|
482
|
+
await guardRedisStartup(`queue "${name}"`, () => newQueue.waitUntilReady(), newQueue);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
void newQueue.close().catch(() => {
|
|
485
|
+
});
|
|
486
|
+
throw err;
|
|
487
|
+
}
|
|
408
488
|
await newQueue.setGlobalConcurrency(queueConcurrency);
|
|
409
489
|
this.queues.push({
|
|
410
490
|
queue: newQueue,
|
|
@@ -2490,6 +2570,8 @@ var createWorkers = async (providers, queues2, config, contexts, evals, tools, t
|
|
|
2490
2570
|
},
|
|
2491
2571
|
maxRetriesPerRequest: null
|
|
2492
2572
|
});
|
|
2573
|
+
logRedisErrors(redisConnection, "worker");
|
|
2574
|
+
await guardRedisStartup("workers", () => redisConnection.ping().then(() => void 0), redisConnection);
|
|
2493
2575
|
}
|
|
2494
2576
|
const workers = queues2.map((queue) => {
|
|
2495
2577
|
console.log(`[EXULU] creating worker for queue ${queue.queue.name}.`);
|
|
@@ -3660,7 +3742,7 @@ var renderTranscript = (segments, speakers) => {
|
|
|
3660
3742
|
|
|
3661
3743
|
// src/exulu/transcription/service.ts
|
|
3662
3744
|
var TABLE = "transcription_jobs";
|
|
3663
|
-
var
|
|
3745
|
+
var log2 = (msg) => console.log(`[EXULU-TRANSCRIPTION] ${msg}`);
|
|
3664
3746
|
var parseJsonField = (v) => {
|
|
3665
3747
|
if (v == null) return null;
|
|
3666
3748
|
if (typeof v === "string") {
|
|
@@ -3731,7 +3813,7 @@ var transcriptionService = {
|
|
|
3731
3813
|
error: err.message,
|
|
3732
3814
|
updatedAt: /* @__PURE__ */ new Date()
|
|
3733
3815
|
}).returning("*");
|
|
3734
|
-
|
|
3816
|
+
log2(`Failed to dispatch job ${row.id}: ${err.message}`);
|
|
3735
3817
|
return this._rowFromDb(failed);
|
|
3736
3818
|
}
|
|
3737
3819
|
},
|
|
@@ -3759,9 +3841,9 @@ var transcriptionService = {
|
|
|
3759
3841
|
updatedAt: /* @__PURE__ */ new Date()
|
|
3760
3842
|
});
|
|
3761
3843
|
} else if (err instanceof TranscriptionServerUnavailable) {
|
|
3762
|
-
|
|
3844
|
+
log2(`Whisper server unreachable while polling ${row.id}; will retry`);
|
|
3763
3845
|
} else {
|
|
3764
|
-
|
|
3846
|
+
log2(`Error polling job ${row.id}: ${err.message}`);
|
|
3765
3847
|
}
|
|
3766
3848
|
}
|
|
3767
3849
|
}
|
|
@@ -3811,7 +3893,7 @@ var transcriptionService = {
|
|
|
3811
3893
|
} catch (err) {
|
|
3812
3894
|
const code = err.code;
|
|
3813
3895
|
if (code !== "JOB_NOT_FOUND") {
|
|
3814
|
-
|
|
3896
|
+
log2(`Best-effort cancel of whisper job failed: ${err.message}`);
|
|
3815
3897
|
}
|
|
3816
3898
|
}
|
|
3817
3899
|
}
|
|
@@ -3897,7 +3979,7 @@ var transcriptionService = {
|
|
|
3897
3979
|
[]
|
|
3898
3980
|
);
|
|
3899
3981
|
} catch (err) {
|
|
3900
|
-
|
|
3982
|
+
log2(`RBAC update failed for item ${itemId}: ${err.message}`);
|
|
3901
3983
|
}
|
|
3902
3984
|
}
|
|
3903
3985
|
const projectId = input.project_id ?? row.project_id ?? null;
|
|
@@ -4185,7 +4267,7 @@ var durationFromSegments = (segments) => {
|
|
|
4185
4267
|
// src/exulu/recall/service.ts
|
|
4186
4268
|
var TABLE2 = "transcription_jobs";
|
|
4187
4269
|
var DEFAULT_BOT_NAME = "Exulu Notetaker";
|
|
4188
|
-
var
|
|
4270
|
+
var log3 = (msg) => console.log(`[EXULU-RECALL] ${msg}`);
|
|
4189
4271
|
var parseJson = (v) => {
|
|
4190
4272
|
if (v == null) return null;
|
|
4191
4273
|
if (typeof v === "string") {
|
|
@@ -4282,7 +4364,7 @@ var recallService = {
|
|
|
4282
4364
|
error: err.message,
|
|
4283
4365
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4284
4366
|
}).returning("*");
|
|
4285
|
-
|
|
4367
|
+
log3(`createBot failed for job ${inserted.id}: ${err.message}`);
|
|
4286
4368
|
return this._row(failed);
|
|
4287
4369
|
}
|
|
4288
4370
|
},
|
|
@@ -4296,7 +4378,7 @@ var recallService = {
|
|
|
4296
4378
|
const name = event?.event ?? "";
|
|
4297
4379
|
const job = await this._findJob(ids.botId, ids.recordingId, ids.transcriptId);
|
|
4298
4380
|
if (!job) {
|
|
4299
|
-
|
|
4381
|
+
log3(`No job for event ${name} (bot=${ids.botId}); ignoring.`);
|
|
4300
4382
|
return;
|
|
4301
4383
|
}
|
|
4302
4384
|
if (name.startsWith("bot.")) {
|
|
@@ -4321,12 +4403,12 @@ var recallService = {
|
|
|
4321
4403
|
await this._fail(job.id, ids.subCode || ids.code || "transcript failed");
|
|
4322
4404
|
return;
|
|
4323
4405
|
default:
|
|
4324
|
-
|
|
4406
|
+
log3(`Unhandled event ${name} for job ${job.id}`);
|
|
4325
4407
|
}
|
|
4326
4408
|
},
|
|
4327
4409
|
async _onRecordingDone(jobId, recordingId) {
|
|
4328
4410
|
if (!recordingId) {
|
|
4329
|
-
|
|
4411
|
+
log3(`recording.done for job ${jobId} had no recording id`);
|
|
4330
4412
|
return;
|
|
4331
4413
|
}
|
|
4332
4414
|
const { db } = await postgresClient();
|
|
@@ -4336,7 +4418,7 @@ var recallService = {
|
|
|
4336
4418
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4337
4419
|
});
|
|
4338
4420
|
if (!claimed) {
|
|
4339
|
-
|
|
4421
|
+
log3(`recording.done for job ${jobId} already handled; skipping.`);
|
|
4340
4422
|
return;
|
|
4341
4423
|
}
|
|
4342
4424
|
try {
|
|
@@ -4356,7 +4438,7 @@ var recallService = {
|
|
|
4356
4438
|
if (!dbRow) return;
|
|
4357
4439
|
const job = this._row(dbRow);
|
|
4358
4440
|
if ((job.status === "awaiting_review" || job.status === "saved") && job.raw_segments && job.raw_segments.length > 0) {
|
|
4359
|
-
|
|
4441
|
+
log3(`transcript.done for job ${jobId} already processed; skipping.`);
|
|
4360
4442
|
return;
|
|
4361
4443
|
}
|
|
4362
4444
|
try {
|
|
@@ -4375,7 +4457,7 @@ var recallService = {
|
|
|
4375
4457
|
const recDuration = recordingDurationSeconds(rec);
|
|
4376
4458
|
if (recDuration != null) duration = recDuration;
|
|
4377
4459
|
} catch (err) {
|
|
4378
|
-
|
|
4460
|
+
log3(`could not fetch recording duration for job ${jobId}: ${err.message}`);
|
|
4379
4461
|
}
|
|
4380
4462
|
}
|
|
4381
4463
|
await this._update(jobId, {
|
|
@@ -4415,11 +4497,11 @@ var recallService = {
|
|
|
4415
4497
|
const prompts = job.post_processing_prompts ?? [];
|
|
4416
4498
|
if (prompts.length === 0) return [];
|
|
4417
4499
|
if (job.post_processing_outputs && job.post_processing_outputs.length > 0) {
|
|
4418
|
-
|
|
4500
|
+
log3(`post-processing for job ${jobId} already ran; skipping.`);
|
|
4419
4501
|
return job.post_processing_outputs;
|
|
4420
4502
|
}
|
|
4421
4503
|
if (!job.raw_segments || job.raw_segments.length === 0) {
|
|
4422
|
-
|
|
4504
|
+
log3(`post-processing for job ${jobId} skipped: no transcript.`);
|
|
4423
4505
|
return [];
|
|
4424
4506
|
}
|
|
4425
4507
|
const outputs = [];
|
|
@@ -4492,7 +4574,7 @@ ${transcriptText}`,
|
|
|
4492
4574
|
ran_at: ranAt
|
|
4493
4575
|
};
|
|
4494
4576
|
} catch (err) {
|
|
4495
|
-
|
|
4577
|
+
log3(`post-processing prompt ${promptId} failed for job ${job.id}: ${err.message}`);
|
|
4496
4578
|
return {
|
|
4497
4579
|
prompt_id: promptId,
|
|
4498
4580
|
agent_id: agentId,
|
|
@@ -8644,6 +8726,52 @@ var verifyRecallRequest = (headers, rawBody, secret = recallVerificationSecret()
|
|
|
8644
8726
|
return passed ? { ok: true } : { ok: false, reason: "signature mismatch" };
|
|
8645
8727
|
};
|
|
8646
8728
|
|
|
8729
|
+
// src/exulu/shared-artifacts.ts
|
|
8730
|
+
import bcrypt3 from "bcryptjs";
|
|
8731
|
+
var normalizeS3Key = (key, bucket) => {
|
|
8732
|
+
const segments = key.split("/").filter((s, i) => !(i === 0 && s === "")).map((s) => decodeURIComponent(s));
|
|
8733
|
+
if (segments[0] === bucket) segments.shift();
|
|
8734
|
+
return segments.join("/");
|
|
8735
|
+
};
|
|
8736
|
+
var isHtmlKey = (key) => /\.html?$/i.test(key);
|
|
8737
|
+
var deriveFilename = (key) => {
|
|
8738
|
+
const base = key.split("/").pop() ?? key;
|
|
8739
|
+
return base.split("_EXULU_").pop() ?? base;
|
|
8740
|
+
};
|
|
8741
|
+
var slugifyShareName = (input) => deriveFilename(input).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8742
|
+
var isExpired = (expiresAt, now) => {
|
|
8743
|
+
if (!expiresAt) return false;
|
|
8744
|
+
return new Date(expiresAt).getTime() <= now.getTime();
|
|
8745
|
+
};
|
|
8746
|
+
var validateCreateInput = (input, now) => {
|
|
8747
|
+
if (!input.s3key) return { ok: false, message: "s3key is required." };
|
|
8748
|
+
if (!input.name) return { ok: false, message: "name is required." };
|
|
8749
|
+
const mode = input.auth_mode;
|
|
8750
|
+
if (mode !== "public" && mode !== "password" && mode !== "regular") {
|
|
8751
|
+
return { ok: false, message: "auth_mode must be public, password, or regular." };
|
|
8752
|
+
}
|
|
8753
|
+
if (mode === "password" && !input.password) {
|
|
8754
|
+
return { ok: false, message: "A password is required for password mode." };
|
|
8755
|
+
}
|
|
8756
|
+
if (input.expires_at && Number.isNaN(new Date(input.expires_at).getTime())) {
|
|
8757
|
+
return { ok: false, message: "expires_at is not a valid date." };
|
|
8758
|
+
}
|
|
8759
|
+
if (input.expires_at && isExpired(input.expires_at, now)) {
|
|
8760
|
+
return { ok: false, message: "expires_at must be in the future." };
|
|
8761
|
+
}
|
|
8762
|
+
return { ok: true };
|
|
8763
|
+
};
|
|
8764
|
+
var hashSharePassword = (password) => bcrypt3.hash(password, 10);
|
|
8765
|
+
var verifySharePassword = (password, hash) => bcrypt3.compare(password, hash);
|
|
8766
|
+
var contentHeadersFor = (key, contentType, filename) => {
|
|
8767
|
+
if (isHtmlKey(key)) return { contentType: "text/html; charset=utf-8" };
|
|
8768
|
+
return {
|
|
8769
|
+
contentType: contentType || "application/octet-stream",
|
|
8770
|
+
disposition: `attachment; filename="${filename.replace(/"/g, "")}"`
|
|
8771
|
+
};
|
|
8772
|
+
};
|
|
8773
|
+
var getSharedArtifactByName = (db, name) => db("shared_artifacts").where({ name }).first();
|
|
8774
|
+
|
|
8647
8775
|
// src/exulu/routes.ts
|
|
8648
8776
|
var REQUEST_SIZE_LIMIT = "50mb";
|
|
8649
8777
|
var getExuluVersionNumber = async () => {
|
|
@@ -11678,6 +11806,126 @@ ${style.markdown}` : params.prompt;
|
|
|
11678
11806
|
}
|
|
11679
11807
|
res.status(204).send();
|
|
11680
11808
|
});
|
|
11809
|
+
app.post("/shared-artifacts", async (req, res) => {
|
|
11810
|
+
const { db } = await postgresClient();
|
|
11811
|
+
const auth = await requestValidators.authenticate(req);
|
|
11812
|
+
if (!auth.user?.id) {
|
|
11813
|
+
res.status(401).json({ detail: "Authentication required." });
|
|
11814
|
+
return;
|
|
11815
|
+
}
|
|
11816
|
+
const now = /* @__PURE__ */ new Date();
|
|
11817
|
+
const valid = validateCreateInput(req.body, now);
|
|
11818
|
+
if (!valid.ok) {
|
|
11819
|
+
res.status(400).json({ detail: valid.message });
|
|
11820
|
+
return;
|
|
11821
|
+
}
|
|
11822
|
+
const bucket = config.fileUploads?.s3Bucket ?? "";
|
|
11823
|
+
const s3key = normalizeS3Key(req.body.s3key, bucket);
|
|
11824
|
+
const name = slugifyShareName(req.body.name);
|
|
11825
|
+
if (!name) {
|
|
11826
|
+
res.status(400).json({ detail: "name must contain url-safe characters." });
|
|
11827
|
+
return;
|
|
11828
|
+
}
|
|
11829
|
+
const existing = await getSharedArtifactByName(db, name);
|
|
11830
|
+
if (existing) {
|
|
11831
|
+
res.status(409).json({ detail: "That share name is already taken." });
|
|
11832
|
+
return;
|
|
11833
|
+
}
|
|
11834
|
+
const auth_mode = req.body.auth_mode;
|
|
11835
|
+
const password_hash = auth_mode === "password" ? await hashSharePassword(req.body.password) : null;
|
|
11836
|
+
const rights_mode = auth_mode === "regular" ? req.body.rights_mode ?? "private" : "public";
|
|
11837
|
+
const [row] = await db("shared_artifacts").insert({
|
|
11838
|
+
name,
|
|
11839
|
+
s3key,
|
|
11840
|
+
auth_mode,
|
|
11841
|
+
password_hash,
|
|
11842
|
+
expires_at: req.body.expires_at ?? null,
|
|
11843
|
+
content_type: req.body.content_type ?? null,
|
|
11844
|
+
rights_mode,
|
|
11845
|
+
created_by: auth.user.id
|
|
11846
|
+
}).returning("*");
|
|
11847
|
+
if (auth_mode === "regular" && req.body.rbac) {
|
|
11848
|
+
await handleRBACUpdate(db, "shared_artifact", row.id, req.body.rbac, []);
|
|
11849
|
+
}
|
|
11850
|
+
res.status(201).json({ name: row.name });
|
|
11851
|
+
});
|
|
11852
|
+
app.get("/shared-artifacts/:name/meta", async (req, res) => {
|
|
11853
|
+
const { db } = await postgresClient();
|
|
11854
|
+
const row = await getSharedArtifactByName(db, req.params.name ?? "");
|
|
11855
|
+
if (!row) {
|
|
11856
|
+
res.status(404).json({ detail: "Not found." });
|
|
11857
|
+
return;
|
|
11858
|
+
}
|
|
11859
|
+
if (isExpired(row.expires_at, /* @__PURE__ */ new Date())) {
|
|
11860
|
+
res.status(410).json({ detail: "This link has expired." });
|
|
11861
|
+
return;
|
|
11862
|
+
}
|
|
11863
|
+
res.json({
|
|
11864
|
+
auth_mode: row.auth_mode,
|
|
11865
|
+
expires_at: row.expires_at,
|
|
11866
|
+
filename: deriveFilename(row.s3key),
|
|
11867
|
+
content_type: row.content_type,
|
|
11868
|
+
is_html: /\.html?$/i.test(row.s3key)
|
|
11869
|
+
});
|
|
11870
|
+
});
|
|
11871
|
+
app.get("/shared-artifacts/:name/content", async (req, res) => {
|
|
11872
|
+
const { db } = await postgresClient();
|
|
11873
|
+
const row = await getSharedArtifactByName(db, req.params.name ?? "");
|
|
11874
|
+
if (!row) {
|
|
11875
|
+
res.status(404).json({ detail: "Not found." });
|
|
11876
|
+
return;
|
|
11877
|
+
}
|
|
11878
|
+
if (isExpired(row.expires_at, /* @__PURE__ */ new Date())) {
|
|
11879
|
+
res.status(410).json({ detail: "This link has expired." });
|
|
11880
|
+
return;
|
|
11881
|
+
}
|
|
11882
|
+
if (row.auth_mode === "password") {
|
|
11883
|
+
const pw = req.headers["x-share-password"] || "";
|
|
11884
|
+
if (!row.password_hash || !await verifySharePassword(pw, row.password_hash)) {
|
|
11885
|
+
res.status(401).json({ detail: "Incorrect password." });
|
|
11886
|
+
return;
|
|
11887
|
+
}
|
|
11888
|
+
} else if (row.auth_mode === "regular") {
|
|
11889
|
+
const viewer = await requestValidators.authenticate(req);
|
|
11890
|
+
if (!viewer.user?.id) {
|
|
11891
|
+
res.status(401).json({ detail: "Authentication required." });
|
|
11892
|
+
return;
|
|
11893
|
+
}
|
|
11894
|
+
const rbac = await RBACResolver(
|
|
11895
|
+
db,
|
|
11896
|
+
"shared_artifact",
|
|
11897
|
+
row.id,
|
|
11898
|
+
row.rights_mode || "private"
|
|
11899
|
+
);
|
|
11900
|
+
const ok = await checkRecordAccess({ ...row, RBAC: rbac }, "read", viewer.user);
|
|
11901
|
+
if (!ok) {
|
|
11902
|
+
res.status(403).json({ detail: "You don't have access to this artifact." });
|
|
11903
|
+
return;
|
|
11904
|
+
}
|
|
11905
|
+
}
|
|
11906
|
+
let bytes;
|
|
11907
|
+
try {
|
|
11908
|
+
bytes = await getS3ObjectBytes(row.s3key, config);
|
|
11909
|
+
} catch (e) {
|
|
11910
|
+
if (e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.$metadata?.httpStatusCode === 404) {
|
|
11911
|
+
res.status(404).json({ detail: "Artifact file not found." });
|
|
11912
|
+
return;
|
|
11913
|
+
}
|
|
11914
|
+
console.error("[EXULU] shared-artifact content read failed", e);
|
|
11915
|
+
res.status(500).json({ detail: "Failed to read artifact." });
|
|
11916
|
+
return;
|
|
11917
|
+
}
|
|
11918
|
+
const headers = contentHeadersFor(
|
|
11919
|
+
row.s3key,
|
|
11920
|
+
row.content_type,
|
|
11921
|
+
deriveFilename(row.s3key)
|
|
11922
|
+
);
|
|
11923
|
+
res.setHeader("Content-Type", headers.contentType);
|
|
11924
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
11925
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
11926
|
+
if (headers.disposition) res.setHeader("Content-Disposition", headers.disposition);
|
|
11927
|
+
res.send(bytes);
|
|
11928
|
+
});
|
|
11681
11929
|
app.use(express2.static("public"));
|
|
11682
11930
|
await registerOpenAIGatewayRoutes(app, providers, tools, contexts, config);
|
|
11683
11931
|
return app;
|
|
@@ -15373,6 +15621,73 @@ var RecursiveChunker = class _RecursiveChunker extends BaseChunker {
|
|
|
15373
15621
|
}
|
|
15374
15622
|
};
|
|
15375
15623
|
|
|
15624
|
+
// src/exulu/read-api.ts
|
|
15625
|
+
var authorizedRead = async (context, user, role, opts = {}) => {
|
|
15626
|
+
if (!opts.itemIds?.length && !opts.externalIds?.length) {
|
|
15627
|
+
throw new Error("authorizedRead requires itemIds or externalIds to constrain the read.");
|
|
15628
|
+
}
|
|
15629
|
+
const { db } = await postgresClient();
|
|
15630
|
+
const table = convertContextToTableDefinition(context);
|
|
15631
|
+
const itemsTable = getTableName(context.id);
|
|
15632
|
+
const chunksTable = getChunksTableName(context.id);
|
|
15633
|
+
const acUser = role && (!user.role || user.role.id !== role) ? { ...user, role: { ...user.role ?? {}, id: role } } : user;
|
|
15634
|
+
let q = db(chunksTable + " as chunks").select([
|
|
15635
|
+
"chunks.id as chunk_id",
|
|
15636
|
+
"chunks.source as chunk_source",
|
|
15637
|
+
"chunks.content as chunk_content",
|
|
15638
|
+
"chunks.chunk_index",
|
|
15639
|
+
"chunks.metadata as chunk_metadata",
|
|
15640
|
+
db.raw('chunks."createdAt" as chunk_created_at'),
|
|
15641
|
+
db.raw('chunks."updatedAt" as chunk_updated_at'),
|
|
15642
|
+
"items.id as item_id",
|
|
15643
|
+
"items.name as item_name",
|
|
15644
|
+
"items.external_id as item_external_id",
|
|
15645
|
+
db.raw('items."createdAt" as item_created_at'),
|
|
15646
|
+
db.raw('items."updatedAt" as item_updated_at')
|
|
15647
|
+
]);
|
|
15648
|
+
q = q.leftJoin(itemsTable + " as items", "chunks.source", "items.id");
|
|
15649
|
+
if (opts.itemIds?.length) q = q.whereIn("items.id", opts.itemIds);
|
|
15650
|
+
if (opts.externalIds?.length) q = q.whereIn("items.external_id", opts.externalIds);
|
|
15651
|
+
if (opts.chunkIndexRange) {
|
|
15652
|
+
const { from, to } = opts.chunkIndexRange;
|
|
15653
|
+
if (typeof from === "number") q = q.where("chunks.chunk_index", ">=", from);
|
|
15654
|
+
if (typeof to === "number") q = q.where("chunks.chunk_index", "<=", to);
|
|
15655
|
+
}
|
|
15656
|
+
q = applyAccessControl(table, q, acUser, "items");
|
|
15657
|
+
q = q.orderBy("chunks.source").orderBy("chunks.chunk_index");
|
|
15658
|
+
return await q;
|
|
15659
|
+
};
|
|
15660
|
+
var entitiesAvailable = async (context) => {
|
|
15661
|
+
if (!context.entities) return false;
|
|
15662
|
+
const { db } = await postgresClient();
|
|
15663
|
+
const table = getChunkEntitiesTableName(context.id);
|
|
15664
|
+
const res = await db.raw(
|
|
15665
|
+
"SELECT to_regclass(?) IS NOT NULL AS exists",
|
|
15666
|
+
[table]
|
|
15667
|
+
);
|
|
15668
|
+
return res.rows?.[0]?.exists === true;
|
|
15669
|
+
};
|
|
15670
|
+
var embedQuery = async (context, text, opts = {}) => {
|
|
15671
|
+
const resolved = await resolveEmbedder({
|
|
15672
|
+
model: context.embedder.model,
|
|
15673
|
+
contextId: context.id,
|
|
15674
|
+
contextName: context.name,
|
|
15675
|
+
user: opts.user,
|
|
15676
|
+
roleId: opts.role
|
|
15677
|
+
});
|
|
15678
|
+
const [vector] = await resolved.embed([text], { inputType: opts.inputType ?? "query" });
|
|
15679
|
+
return vector ?? [];
|
|
15680
|
+
};
|
|
15681
|
+
var ExuluReadApi = {
|
|
15682
|
+
getTableName,
|
|
15683
|
+
getChunksTableName,
|
|
15684
|
+
getEntitiesTableName,
|
|
15685
|
+
getChunkEntitiesTableName,
|
|
15686
|
+
entitiesAvailable,
|
|
15687
|
+
authorizedRead,
|
|
15688
|
+
embedQuery
|
|
15689
|
+
};
|
|
15690
|
+
|
|
15376
15691
|
// src/postgres/init-exulu-db.ts
|
|
15377
15692
|
var {
|
|
15378
15693
|
agentsSchema: agentsSchema2,
|
|
@@ -15400,7 +15715,8 @@ var {
|
|
|
15400
15715
|
promptFavoritesSchema: promptFavoritesSchema2,
|
|
15401
15716
|
transcriptionJobsSchema: transcriptionJobsSchema2,
|
|
15402
15717
|
imageGenerationsSchema,
|
|
15403
|
-
oauthTokensSchema
|
|
15718
|
+
oauthTokensSchema,
|
|
15719
|
+
sharedArtifactsSchema
|
|
15404
15720
|
} = coreSchemas.get();
|
|
15405
15721
|
var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
|
|
15406
15722
|
for (const field of fields) {
|
|
@@ -15444,6 +15760,7 @@ var up = async function(knex) {
|
|
|
15444
15760
|
transcriptionJobsSchema2(),
|
|
15445
15761
|
imageGenerationsSchema(),
|
|
15446
15762
|
oauthTokensSchema(),
|
|
15763
|
+
sharedArtifactsSchema(),
|
|
15447
15764
|
rbacSchema2(),
|
|
15448
15765
|
agentsSchema2(),
|
|
15449
15766
|
feedbackSchema2(),
|
|
@@ -15729,7 +16046,7 @@ var checkLiteLLMDatabaseSafety = (configPath) => {
|
|
|
15729
16046
|
|
|
15730
16047
|
// src/exulu/litellm/db-init.ts
|
|
15731
16048
|
var WARNING_BANNER = "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550";
|
|
15732
|
-
var
|
|
16049
|
+
var warn2 = (lines) => {
|
|
15733
16050
|
console.warn(`
|
|
15734
16051
|
${WARNING_BANNER}`);
|
|
15735
16052
|
console.warn("\u26A0 [EXULU-LITELLM] CONFIGURATION WARNING");
|
|
@@ -15737,13 +16054,13 @@ ${WARNING_BANNER}`);
|
|
|
15737
16054
|
console.warn(`${WARNING_BANNER}
|
|
15738
16055
|
`);
|
|
15739
16056
|
};
|
|
15740
|
-
var
|
|
16057
|
+
var log4 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
|
|
15741
16058
|
var initLiteLLMDatabase = async (packageRoot) => {
|
|
15742
16059
|
const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve2(process.cwd(), "./config.litellm.yaml");
|
|
15743
16060
|
const safety = checkLiteLLMDatabaseSafety(configPath);
|
|
15744
16061
|
if (safety.ok && safety.reason === "no-litellm-db-mode") return;
|
|
15745
16062
|
if (!safety.ok && safety.reason === "unparseable-url") {
|
|
15746
|
-
|
|
16063
|
+
warn2([
|
|
15747
16064
|
`LiteLLM's database_url is not a valid postgres URL:`,
|
|
15748
16065
|
` ${safety.rawUrl}`,
|
|
15749
16066
|
`Expected postgres://user:pass@host:port/database. Skipping setup.`
|
|
@@ -15752,7 +16069,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15752
16069
|
}
|
|
15753
16070
|
if (!safety.ok && safety.reason === "shared-with-exulu") {
|
|
15754
16071
|
const { exuluTarget } = safety;
|
|
15755
|
-
|
|
16072
|
+
warn2([
|
|
15756
16073
|
`LiteLLM's database_url points to the SAME database Exulu uses:`,
|
|
15757
16074
|
` ${exuluTarget.host}:${exuluTarget.port}/${exuluTarget.database}`,
|
|
15758
16075
|
``,
|
|
@@ -15771,7 +16088,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15771
16088
|
return;
|
|
15772
16089
|
}
|
|
15773
16090
|
const target = "litellmTarget" in safety ? safety.litellmTarget : void 0;
|
|
15774
|
-
|
|
16091
|
+
log4(
|
|
15775
16092
|
`LiteLLM database mode detected (${target?.host}:${target?.port}/${target?.database}).`
|
|
15776
16093
|
);
|
|
15777
16094
|
const ensureDatabaseExists = async () => {
|
|
@@ -15783,7 +16100,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15783
16100
|
} catch (err) {
|
|
15784
16101
|
const code = err?.code;
|
|
15785
16102
|
if (code !== "3D000") {
|
|
15786
|
-
|
|
16103
|
+
warn2([
|
|
15787
16104
|
`Could not connect to LiteLLM's target database:`,
|
|
15788
16105
|
` ${err instanceof Error ? err.message : String(err)}`,
|
|
15789
16106
|
``,
|
|
@@ -15795,13 +16112,13 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15795
16112
|
const url = new URL(litellmUrl);
|
|
15796
16113
|
const targetDbName = url.pathname.replace(/^\//, "");
|
|
15797
16114
|
if (!targetDbName) {
|
|
15798
|
-
|
|
16115
|
+
warn2([`LiteLLM database_url has no database name; cannot auto-create.`]);
|
|
15799
16116
|
return false;
|
|
15800
16117
|
}
|
|
15801
16118
|
url.pathname = "/postgres";
|
|
15802
|
-
|
|
16119
|
+
log4(`Target database "${targetDbName}" does not exist; creating it\u2026`);
|
|
15803
16120
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(targetDbName)) {
|
|
15804
|
-
|
|
16121
|
+
warn2([
|
|
15805
16122
|
`Refusing to auto-create database "${targetDbName}" \u2014 name`,
|
|
15806
16123
|
`contains characters that would require quoting. Create it`,
|
|
15807
16124
|
`manually: createdb -h ${url.hostname} -U ${url.username} ${targetDbName}`
|
|
@@ -15812,10 +16129,10 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15812
16129
|
try {
|
|
15813
16130
|
await admin.connect();
|
|
15814
16131
|
await admin.query(`CREATE DATABASE "${targetDbName}"`);
|
|
15815
|
-
|
|
16132
|
+
log4(`\u2713 Created database "${targetDbName}".`);
|
|
15816
16133
|
return true;
|
|
15817
16134
|
} catch (createErr) {
|
|
15818
|
-
|
|
16135
|
+
warn2([
|
|
15819
16136
|
`Failed to auto-create database "${targetDbName}":`,
|
|
15820
16137
|
` ${createErr instanceof Error ? createErr.message : String(createErr)}`,
|
|
15821
16138
|
``,
|
|
@@ -15834,7 +16151,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15834
16151
|
}
|
|
15835
16152
|
};
|
|
15836
16153
|
if (!await ensureDatabaseExists()) return;
|
|
15837
|
-
|
|
16154
|
+
log4("Checking that the target database is safe to push into\u2026");
|
|
15838
16155
|
const client2 = new Client({ connectionString: litellmUrl });
|
|
15839
16156
|
let foreignTables = [];
|
|
15840
16157
|
try {
|
|
@@ -15850,7 +16167,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15850
16167
|
);
|
|
15851
16168
|
foreignTables = res.rows.map((r) => r.table_name);
|
|
15852
16169
|
} catch (err) {
|
|
15853
|
-
|
|
16170
|
+
warn2([
|
|
15854
16171
|
`Could not query LiteLLM's target database to verify it is safe`,
|
|
15855
16172
|
`to push into:`,
|
|
15856
16173
|
` ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -15866,7 +16183,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15866
16183
|
}
|
|
15867
16184
|
}
|
|
15868
16185
|
if (foreignTables.length > 0) {
|
|
15869
|
-
|
|
16186
|
+
warn2([
|
|
15870
16187
|
`LiteLLM's target database contains ${foreignTables.length} table(s) that are NOT`,
|
|
15871
16188
|
`part of LiteLLM's schema:`,
|
|
15872
16189
|
...foreignTables.slice(0, 10).map((t) => ` - ${t}`),
|
|
@@ -15882,7 +16199,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15882
16199
|
const venvLibDir = resolve2(packageRoot, "ee/python/.venv/lib");
|
|
15883
16200
|
const pythonVersionDir = existsSync5(venvLibDir) ? readdirSync(venvLibDir).find((entry) => /^python3\.\d+$/.test(entry)) : void 0;
|
|
15884
16201
|
if (!pythonVersionDir) {
|
|
15885
|
-
|
|
16202
|
+
warn2([
|
|
15886
16203
|
`Could not find a python3.* directory under ${venvLibDir}.`,
|
|
15887
16204
|
`Run \`npm run python:setup\` to create the venv.`,
|
|
15888
16205
|
`Skipping LiteLLM database setup.`
|
|
@@ -15896,7 +16213,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15896
16213
|
);
|
|
15897
16214
|
const schemaPath = resolve2(litellmProxyDir, "schema.prisma");
|
|
15898
16215
|
if (!existsSync5(prismaCli)) {
|
|
15899
|
-
|
|
16216
|
+
warn2([
|
|
15900
16217
|
`Prisma CLI not found at ${prismaCli}.`,
|
|
15901
16218
|
`Run \`npm run python:setup\` to create the venv and install prisma.`,
|
|
15902
16219
|
`Skipping LiteLLM database setup.`
|
|
@@ -15904,13 +16221,13 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15904
16221
|
return;
|
|
15905
16222
|
}
|
|
15906
16223
|
if (!existsSync5(schemaPath)) {
|
|
15907
|
-
|
|
16224
|
+
warn2([
|
|
15908
16225
|
`LiteLLM Prisma schema not found at ${schemaPath}.`,
|
|
15909
16226
|
`Re-run \`npm run python:setup\`. Skipping LiteLLM database setup.`
|
|
15910
16227
|
]);
|
|
15911
16228
|
return;
|
|
15912
16229
|
}
|
|
15913
|
-
|
|
16230
|
+
log4("Running `prisma db push` against LiteLLM's schema\u2026");
|
|
15914
16231
|
const result = spawnSync(prismaCli, ["db", "push", "--skip-generate"], {
|
|
15915
16232
|
cwd: litellmProxyDir,
|
|
15916
16233
|
env: {
|
|
@@ -15924,14 +16241,14 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15924
16241
|
encoding: "utf8"
|
|
15925
16242
|
});
|
|
15926
16243
|
if (result.error) {
|
|
15927
|
-
|
|
16244
|
+
warn2([
|
|
15928
16245
|
`Failed to launch prisma: ${result.error.message}`,
|
|
15929
16246
|
`Skipping LiteLLM database setup.`
|
|
15930
16247
|
]);
|
|
15931
16248
|
return;
|
|
15932
16249
|
}
|
|
15933
16250
|
if (result.status !== 0) {
|
|
15934
|
-
|
|
16251
|
+
warn2([
|
|
15935
16252
|
`prisma db push exited with status ${result.status}.`,
|
|
15936
16253
|
`stdout:`,
|
|
15937
16254
|
...(result.stdout || "(empty)").split("\n").map((l) => ` ${l}`),
|
|
@@ -15940,7 +16257,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
15940
16257
|
]);
|
|
15941
16258
|
return;
|
|
15942
16259
|
}
|
|
15943
|
-
|
|
16260
|
+
log4("\u2713 LiteLLM database ready.");
|
|
15944
16261
|
};
|
|
15945
16262
|
|
|
15946
16263
|
// src/postgres/init-litellm-db.ts
|
|
@@ -17207,14 +17524,37 @@ ${setupResult.output || ""}`);
|
|
|
17207
17524
|
model: config.processor.model ?? "mistral-ocr",
|
|
17208
17525
|
...config.attribution
|
|
17209
17526
|
});
|
|
17210
|
-
|
|
17211
|
-
const
|
|
17212
|
-
const
|
|
17213
|
-
|
|
17214
|
-
|
|
17215
|
-
|
|
17216
|
-
|
|
17217
|
-
|
|
17527
|
+
const maxPagesPerChunk = config.processor.maxPagesPerChunk ?? 25;
|
|
17528
|
+
const chunksDir = path.join(path.dirname(paths.json), "ocr_chunks");
|
|
17529
|
+
const splitResult = await executePythonScript({
|
|
17530
|
+
scriptPath: "ee/python/documents/processing/split_pdf.py",
|
|
17531
|
+
args: [paths.source, chunksDir, "--chunk-size", String(maxPagesPerChunk)],
|
|
17532
|
+
timeout: 5 * 60 * 1e3
|
|
17533
|
+
});
|
|
17534
|
+
const pdfChunks = JSON.parse(splitResult.stdout);
|
|
17535
|
+
console.log(`[EXULU] PDF split into ${pdfChunks.length} chunk(s) for OCR (max ${maxPagesPerChunk} pages each)`);
|
|
17536
|
+
const chunkLimit = pLimit(3);
|
|
17537
|
+
const chunkResults = await Promise.all(
|
|
17538
|
+
pdfChunks.map(
|
|
17539
|
+
(chunk, i) => chunkLimit(async () => {
|
|
17540
|
+
await new Promise((resolve4) => setImmediate(resolve4));
|
|
17541
|
+
await new Promise((resolve4) => setTimeout(resolve4, Math.floor(Math.random() * 1e3) + 200));
|
|
17542
|
+
console.log(`[EXULU] OCR chunk ${i + 1}/${pdfChunks.length}: pages ${chunk.start_page}\u2013${chunk.end_page - 1}`);
|
|
17543
|
+
const chunkBuffer = await fs3.promises.readFile(chunk.path);
|
|
17544
|
+
const chunkBase64 = chunkBuffer.toString("base64");
|
|
17545
|
+
const chunkResponse = await withRetry(async () => {
|
|
17546
|
+
return await resolved.ocr({
|
|
17547
|
+
type: "document_url",
|
|
17548
|
+
document_url: "data:application/pdf;base64," + chunkBase64
|
|
17549
|
+
}, { includeImageBase64: false });
|
|
17550
|
+
}, 10);
|
|
17551
|
+
return { pages: chunkResponse.pages, offset: chunk.start_page };
|
|
17552
|
+
})
|
|
17553
|
+
)
|
|
17554
|
+
);
|
|
17555
|
+
const mergedPages = chunkResults.sort((a, b) => a.offset - b.offset).flatMap(
|
|
17556
|
+
({ pages, offset }) => pages.map((p) => ({ ...p, index: p.index + offset }))
|
|
17557
|
+
);
|
|
17218
17558
|
const parser = new LiteParse();
|
|
17219
17559
|
const screenshots = await parser.screenshot(paths.source, void 0);
|
|
17220
17560
|
await fs3.promises.mkdir(paths.images, { recursive: true });
|
|
@@ -17228,7 +17568,7 @@ ${setupResult.output || ""}`);
|
|
|
17228
17568
|
);
|
|
17229
17569
|
screenshot.imagePath = path.join(paths.images, `${screenshot.pageNum}.png`);
|
|
17230
17570
|
}
|
|
17231
|
-
json =
|
|
17571
|
+
json = mergedPages.map((page) => ({
|
|
17232
17572
|
page: page.index + 1,
|
|
17233
17573
|
content: page.markdown,
|
|
17234
17574
|
image: screenshots.find((s) => s.pageNum === page.index + 1)?.imagePath,
|
|
@@ -17557,9 +17897,11 @@ export {
|
|
|
17557
17897
|
ExuluProvider,
|
|
17558
17898
|
ExuluPython,
|
|
17559
17899
|
queues as ExuluQueues,
|
|
17900
|
+
ExuluReadApi,
|
|
17560
17901
|
ExuluReranker,
|
|
17561
17902
|
ExuluTool,
|
|
17562
17903
|
trajectoryRegistry as ExuluTrajectoryRegistry,
|
|
17563
17904
|
ExuluVariables,
|
|
17564
|
-
defaultChunker
|
|
17905
|
+
defaultChunker,
|
|
17906
|
+
postgresClient
|
|
17565
17907
|
};
|