@exulu/backend 3.7.4 → 4.1.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-27K2CO47.js → chunk-QMN6MVHQ.js} +1 -1
- package/dist/{chunk-AWMU6QXB.js → chunk-RBEWHG7I.js} +297 -39
- package/dist/cli/start-whisper.js +1 -1
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-XNQ6Q3X6.js → convert-exulu-tools-to-ai-sdk-tools-6RU4IZMI.js} +1 -1
- package/dist/index.cjs +617 -223
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +300 -186
- package/dist/{python-setup-JZGHWQCG.js → python-setup-DRJ3QX5F.js} +1 -1
- package/ee/LICENSE.md +2 -2
- package/ee/agentic-retrieval/pipeline/config.test.ts +18 -1
- package/ee/agentic-retrieval/pipeline/config.ts +15 -0
- package/ee/agentic-retrieval/pipeline/index.test.ts +73 -0
- package/ee/agentic-retrieval/pipeline/index.ts +67 -13
- package/ee/agentic-retrieval/pipeline/memory.test.ts +59 -0
- package/ee/agentic-retrieval/pipeline/memory.ts +181 -11
- package/ee/agentic-retrieval/pipeline/pin-rerun.test.ts +17 -0
- package/ee/agentic-retrieval/pipeline/pin-rerun.ts +29 -0
- package/ee/agentic-retrieval/pipeline/routing.test.ts +34 -0
- package/ee/agentic-retrieval/pipeline/routing.ts +96 -5
- package/ee/agentic-retrieval/pipeline/search.ts +9 -6
- package/ee/agentic-retrieval/pipeline/timing.test.ts +24 -0
- package/ee/agentic-retrieval/pipeline/timing.ts +26 -0
- package/ee/agentic-retrieval/pipeline/types.ts +2 -0
- package/ee/python/documents/processing/README.md +2 -3
- package/ee/python/documents/processing/doc_processor.ts +21 -61
- package/ee/python/documents/processing/split_pdf.py +25 -30
- package/ee/python/documents/processing/tests/__init__.py +0 -0
- package/ee/python/documents/processing/tests/test_split_pdf.py +230 -0
- package/ee/python/requirements.txt +12 -2
- package/ee/python/setup.sh +40 -1
- package/ee/python/transcription/pipeline.py +109 -15
- package/ee/python/transcription/tests/test_align_model_licensing.py +184 -0
- package/ee/workers.ts +2 -7
- package/license.md +2 -2
- package/package.json +3 -4
- package/scripts/postinstall.cjs +52 -1
- package/ee/python/documents/processing/document_to_markdown.py +0 -413
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
isPythonEnvironmentSetup,
|
|
6
6
|
setupPythonEnvironment,
|
|
7
7
|
validatePythonEnvironment
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QMN6MVHQ.js";
|
|
9
9
|
import {
|
|
10
10
|
COMPACTION_INSUFFICIENT,
|
|
11
11
|
ContextCompactionRequiredError,
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
imageAttachmentGuard,
|
|
63
63
|
initAudit,
|
|
64
64
|
invalidateBudgetCaches,
|
|
65
|
+
isIgnoredArtifactPath,
|
|
65
66
|
isLiteLLMEnabled,
|
|
66
67
|
listS3ObjectsByPrefix,
|
|
67
68
|
listTagsByPrefix,
|
|
@@ -75,6 +76,7 @@ import {
|
|
|
75
76
|
resolveReranker,
|
|
76
77
|
sanitizeName,
|
|
77
78
|
sanitizeToolName,
|
|
79
|
+
sessionFilePrefix,
|
|
78
80
|
setBudgetSettings,
|
|
79
81
|
setLiteLLMPackageRoot,
|
|
80
82
|
sliceHistoryAtCheckpoint,
|
|
@@ -88,7 +90,7 @@ import {
|
|
|
88
90
|
verifyCredentialNonce,
|
|
89
91
|
waitForLiteLLMReady,
|
|
90
92
|
withRetry
|
|
91
|
-
} from "./chunk-
|
|
93
|
+
} from "./chunk-RBEWHG7I.js";
|
|
92
94
|
import {
|
|
93
95
|
LiteLLMAdminError,
|
|
94
96
|
findLiteLLMModel,
|
|
@@ -1418,6 +1420,67 @@ function resolveSearchQueryTexts(query) {
|
|
|
1418
1420
|
hybridOrQuery: buildFullTextOrQuery(query, ftsText)
|
|
1419
1421
|
};
|
|
1420
1422
|
}
|
|
1423
|
+
function chooseFullTextQuery(opts) {
|
|
1424
|
+
if (opts.strictMatches || countOrTerms(opts.orText) > MAX_OR_TERMS) {
|
|
1425
|
+
return { fn: "plainto_tsquery", text: opts.strictText };
|
|
1426
|
+
}
|
|
1427
|
+
return { fn: "websearch_to_tsquery", text: opts.orText };
|
|
1428
|
+
}
|
|
1429
|
+
var MAX_OR_TERMS = 12;
|
|
1430
|
+
function countOrTerms(orText) {
|
|
1431
|
+
return orText.trim() ? orText.split(/\s+or\s+/i).length : 0;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/graphql/resolvers/expand-neighbours.ts
|
|
1435
|
+
function planNeighbourFetch(results, expand) {
|
|
1436
|
+
const before = Math.max(0, expand.before ?? 0);
|
|
1437
|
+
const after = Math.max(0, expand.after ?? 0);
|
|
1438
|
+
const plan = /* @__PURE__ */ new Map();
|
|
1439
|
+
if (before === 0 && after === 0) return plan;
|
|
1440
|
+
const present = new Set(results.map((r) => `${r.item_id}-${r.chunk_index}`));
|
|
1441
|
+
for (const r of results) {
|
|
1442
|
+
for (let i = r.chunk_index - before; i <= r.chunk_index + after; i++) {
|
|
1443
|
+
if (i < 0 || i === r.chunk_index || present.has(`${r.item_id}-${i}`)) continue;
|
|
1444
|
+
if (!plan.has(r.item_id)) plan.set(r.item_id, /* @__PURE__ */ new Set());
|
|
1445
|
+
plan.get(r.item_id).add(i);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return plan;
|
|
1449
|
+
}
|
|
1450
|
+
function mergeNeighbours(results, rows, plan, context) {
|
|
1451
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
1452
|
+
for (const r of results) if (!byItem.has(r.item_id)) byItem.set(r.item_id, r);
|
|
1453
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1454
|
+
for (const r of results) merged.set(`${r.item_id}-${r.chunk_index}`, r);
|
|
1455
|
+
for (const row of rows) {
|
|
1456
|
+
if (!plan.get(row.source)?.has(row.chunk_index)) continue;
|
|
1457
|
+
const key = `${row.source}-${row.chunk_index}`;
|
|
1458
|
+
if (merged.has(key)) continue;
|
|
1459
|
+
const parent = byItem.get(row.source);
|
|
1460
|
+
if (!parent) continue;
|
|
1461
|
+
merged.set(key, {
|
|
1462
|
+
chunk_content: row.content,
|
|
1463
|
+
chunk_index: row.chunk_index,
|
|
1464
|
+
chunk_id: row.id,
|
|
1465
|
+
chunk_source: row.source,
|
|
1466
|
+
chunk_metadata: row.metadata,
|
|
1467
|
+
chunk_created_at: row.createdAt,
|
|
1468
|
+
chunk_updated_at: row.updatedAt,
|
|
1469
|
+
item_updated_at: parent.item_updated_at,
|
|
1470
|
+
item_created_at: parent.item_created_at,
|
|
1471
|
+
item_id: parent.item_id,
|
|
1472
|
+
item_external_id: parent.item_external_id,
|
|
1473
|
+
item_name: parent.item_name,
|
|
1474
|
+
chunk_cosine_distance: 0,
|
|
1475
|
+
chunk_fts_rank: 0,
|
|
1476
|
+
chunk_hybrid_score: 0,
|
|
1477
|
+
context
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
return Array.from(merged.values()).sort(
|
|
1481
|
+
(a, b) => a.item_id === b.item_id ? a.chunk_index - b.chunk_index : 0
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1421
1484
|
|
|
1422
1485
|
// src/graphql/resolvers/field-allow-list.ts
|
|
1423
1486
|
var ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt"]);
|
|
@@ -2385,6 +2448,14 @@ var agentsSchema = {
|
|
|
2385
2448
|
name: "max_tool_steps",
|
|
2386
2449
|
type: "number"
|
|
2387
2450
|
},
|
|
2451
|
+
{
|
|
2452
|
+
// Thinking budget of the answer model, forwarded as LiteLLM's
|
|
2453
|
+
// reasoning_effort ("none" | "disable" | "minimal" | "low" | "medium" |
|
|
2454
|
+
// "high"). null = provider default. See resolve-reasoning-effort.ts.
|
|
2455
|
+
// Auto-ALTERed on boot.
|
|
2456
|
+
name: "reasoning_effort",
|
|
2457
|
+
type: "text"
|
|
2458
|
+
},
|
|
2388
2459
|
{
|
|
2389
2460
|
name: "guest_access",
|
|
2390
2461
|
type: "boolean",
|
|
@@ -3099,6 +3170,14 @@ var convertContextToTableDefinition = (context) => {
|
|
|
3099
3170
|
return addCoreFields(definition);
|
|
3100
3171
|
};
|
|
3101
3172
|
|
|
3173
|
+
// src/graphql/resolvers/query-embedding-policy.ts
|
|
3174
|
+
function needsQueryEmbedding(method) {
|
|
3175
|
+
return method !== "tsvector";
|
|
3176
|
+
}
|
|
3177
|
+
function boostsWithQueryEntities(method) {
|
|
3178
|
+
return method !== "tsvector";
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3102
3181
|
// src/exulu/entities/config.ts
|
|
3103
3182
|
var hydrateEntityTypes = async (context) => {
|
|
3104
3183
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -3863,7 +3942,9 @@ var vectorSearch = async ({
|
|
|
3863
3942
|
const embedText = texts.embedText;
|
|
3864
3943
|
hybridOrQuery = texts.hybridOrQuery;
|
|
3865
3944
|
query = texts.ftsText;
|
|
3866
|
-
if (
|
|
3945
|
+
if (!needsQueryEmbedding(method)) {
|
|
3946
|
+
_embedSource = "none";
|
|
3947
|
+
} else if (queryEmbedding && queryEmbedding.length) {
|
|
3867
3948
|
vector = queryEmbedding;
|
|
3868
3949
|
_embedSource = "reused";
|
|
3869
3950
|
} else {
|
|
@@ -3896,8 +3977,10 @@ var vectorSearch = async ({
|
|
|
3896
3977
|
vector = queryVector;
|
|
3897
3978
|
_embedSource = "computed";
|
|
3898
3979
|
}
|
|
3899
|
-
|
|
3900
|
-
|
|
3980
|
+
if (vector.length) {
|
|
3981
|
+
vectorStr = `ARRAY[${vector.join(",")}]`;
|
|
3982
|
+
vectorExpr = `${vectorStr}::vector`;
|
|
3983
|
+
}
|
|
3901
3984
|
}
|
|
3902
3985
|
let keywordsQuery = [];
|
|
3903
3986
|
if (keywords) {
|
|
@@ -3955,14 +4038,22 @@ var vectorSearch = async ({
|
|
|
3955
4038
|
]);
|
|
3956
4039
|
resultChunks = await chunksQuery;
|
|
3957
4040
|
break;
|
|
3958
|
-
case "hybridSearch":
|
|
4041
|
+
case "hybridSearch": {
|
|
4042
|
+
let strictMatches = false;
|
|
4043
|
+
if (query && hybridOrQuery) {
|
|
4044
|
+
const probe = await db(chunksTable + " as chunks").select(db.raw("1")).whereRaw(`(${languages.map((lang) => `chunks.fts @@ plainto_tsquery('${lang}', ?)`).join(" OR ")})`, languages.map(() => query)).first();
|
|
4045
|
+
strictMatches = Boolean(probe);
|
|
4046
|
+
}
|
|
4047
|
+
const fullText = chooseFullTextQuery({ strictMatches, strictText: query ?? "", orText: hybridOrQuery });
|
|
4048
|
+
const ftsFn = fullText.fn;
|
|
4049
|
+
hybridOrQuery = fullText.text;
|
|
3959
4050
|
const matchCount = Math.min(limit * 2);
|
|
3960
4051
|
const fullTextWeight = 2;
|
|
3961
4052
|
const semanticWeight = 1;
|
|
3962
4053
|
const rrfK = 50;
|
|
3963
|
-
const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts,
|
|
4054
|
+
const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ");
|
|
3964
4055
|
const ftRankParams = languages.map(() => hybridOrQuery);
|
|
3965
|
-
const ftMatchExpression = languages.map((lang) => `chunks.fts @@
|
|
4056
|
+
const ftMatchExpression = languages.map((lang) => `chunks.fts @@ ${ftsFn}('${lang}', ?)`).join(" OR ");
|
|
3966
4057
|
const ftMatchParams = languages.map(() => hybridOrQuery);
|
|
3967
4058
|
let fullTextQuery = db(chunksTable + " as chunks").select([
|
|
3968
4059
|
"chunks.id",
|
|
@@ -4003,7 +4094,7 @@ var vectorSearch = async ({
|
|
|
4003
4094
|
db.raw('items."updatedAt" as item_updated_at'),
|
|
4004
4095
|
db.raw('items."createdAt" as item_created_at'),
|
|
4005
4096
|
db.raw(
|
|
4006
|
-
`GREATEST(${languages.map((lang) => `ts_rank(chunks.fts,
|
|
4097
|
+
`GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ")}) AS fts_rank`,
|
|
4007
4098
|
languages.map(() => hybridOrQuery)
|
|
4008
4099
|
),
|
|
4009
4100
|
db.raw(`(1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance`),
|
|
@@ -4029,12 +4120,14 @@ var vectorSearch = async ({
|
|
|
4029
4120
|
`,
|
|
4030
4121
|
[rrfK, fullTextWeight, rrfK, semanticWeight, cutoffs?.hybrid || 0]
|
|
4031
4122
|
).whereRaw(
|
|
4032
|
-
`(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts,
|
|
4123
|
+
`(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ")}) > ?)`,
|
|
4033
4124
|
[...languages.map(() => hybridOrQuery), cutoffs?.tsvector || 0]
|
|
4034
4125
|
).whereRaw(`(chunks.embedding IS NULL OR (1 - (chunks.embedding <=> ${vectorExpr})) >= ?)`, [
|
|
4035
4126
|
cutoffs?.cosineDistance || 0
|
|
4036
4127
|
]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
|
|
4037
4128
|
resultChunks = await hybridQuery;
|
|
4129
|
+
break;
|
|
4130
|
+
}
|
|
4038
4131
|
}
|
|
4039
4132
|
if (process.env.EXULU_VS_TIMING) {
|
|
4040
4133
|
console.log(
|
|
@@ -4086,7 +4179,7 @@ var vectorSearch = async ({
|
|
|
4086
4179
|
}
|
|
4087
4180
|
let queryEntities = [];
|
|
4088
4181
|
let entityInsights;
|
|
4089
|
-
if (entitiesOn && rawQuery) {
|
|
4182
|
+
if (entitiesOn && rawQuery && boostsWithQueryEntities(method)) {
|
|
4090
4183
|
try {
|
|
4091
4184
|
const types = await hydrateEntityTypes(context);
|
|
4092
4185
|
const { mentions: queryMentions } = await extractEntitiesForItem({
|
|
@@ -4118,104 +4211,18 @@ var vectorSearch = async ({
|
|
|
4118
4211
|
}
|
|
4119
4212
|
results = results.slice(0, limit);
|
|
4120
4213
|
if (expand?.before || expand?.after) {
|
|
4121
|
-
const
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
const indicesToFetch = Array.from(
|
|
4128
|
-
{ length: expand.before },
|
|
4129
|
-
(_, i) => chunk.chunk_index - expand.before + i
|
|
4130
|
-
).filter((index) => index >= 0);
|
|
4131
|
-
await Promise.all(
|
|
4132
|
-
indicesToFetch.map(async (index) => {
|
|
4133
|
-
if (expandedMap.has(`${chunk.item_id}-${index}`)) {
|
|
4134
|
-
return;
|
|
4135
|
-
}
|
|
4136
|
-
const expandedChunk = await db(chunksTable).where({
|
|
4137
|
-
source: chunk.item_id,
|
|
4138
|
-
chunk_index: index
|
|
4139
|
-
}).first();
|
|
4140
|
-
if (expandedChunk) {
|
|
4141
|
-
if (expandedChunk) {
|
|
4142
|
-
expandedMap.set(`${chunk.item_id}-${index}`, {
|
|
4143
|
-
chunk_content: expandedChunk.content,
|
|
4144
|
-
chunk_index: expandedChunk.chunk_index,
|
|
4145
|
-
chunk_id: expandedChunk.id,
|
|
4146
|
-
chunk_source: expandedChunk.source,
|
|
4147
|
-
chunk_metadata: expandedChunk.metadata,
|
|
4148
|
-
chunk_created_at: expandedChunk.createdAt,
|
|
4149
|
-
chunk_updated_at: expandedChunk.updatedAt,
|
|
4150
|
-
item_updated_at: chunk.item_updated_at,
|
|
4151
|
-
item_created_at: chunk.item_created_at,
|
|
4152
|
-
item_id: chunk.item_id,
|
|
4153
|
-
item_external_id: chunk.item_external_id,
|
|
4154
|
-
item_name: chunk.item_name,
|
|
4155
|
-
chunk_cosine_distance: 0,
|
|
4156
|
-
chunk_fts_rank: 0,
|
|
4157
|
-
chunk_hybrid_score: 0,
|
|
4158
|
-
context: {
|
|
4159
|
-
name: table.name.singular,
|
|
4160
|
-
id: table.id || ""
|
|
4161
|
-
}
|
|
4162
|
-
});
|
|
4163
|
-
}
|
|
4164
|
-
}
|
|
4165
|
-
})
|
|
4166
|
-
);
|
|
4167
|
-
}
|
|
4214
|
+
const plan = planNeighbourFetch(results, expand);
|
|
4215
|
+
if (plan.size > 0) {
|
|
4216
|
+
const itemIds = Array.from(plan.keys());
|
|
4217
|
+
const indices = Array.from(new Set(Array.from(plan.values()).flatMap((s) => Array.from(s))));
|
|
4218
|
+
const rows = await db(chunksTable).select(["id", "source", "chunk_index", "content", "metadata", "createdAt", "updatedAt"]).whereIn("source", itemIds).whereIn("chunk_index", indices);
|
|
4219
|
+
results = mergeNeighbours(results, rows, plan, { name: table.name.singular, id: table.id || "" });
|
|
4168
4220
|
}
|
|
4169
|
-
if (expand?.after) {
|
|
4170
|
-
for (const chunk of results) {
|
|
4171
|
-
const indicesToFetch = Array.from(
|
|
4172
|
-
{ length: expand.after },
|
|
4173
|
-
(_, i) => chunk.chunk_index + i + 1
|
|
4174
|
-
);
|
|
4175
|
-
await Promise.all(
|
|
4176
|
-
indicesToFetch.map(async (index) => {
|
|
4177
|
-
if (expandedMap.has(`${chunk.item_id}-${index}`)) {
|
|
4178
|
-
return;
|
|
4179
|
-
}
|
|
4180
|
-
const expandedChunk = await db(chunksTable).where({
|
|
4181
|
-
source: chunk.item_id,
|
|
4182
|
-
chunk_index: index
|
|
4183
|
-
}).first();
|
|
4184
|
-
if (expandedChunk) {
|
|
4185
|
-
expandedMap.set(`${chunk.item_id}-${index}`, {
|
|
4186
|
-
chunk_content: expandedChunk.content,
|
|
4187
|
-
chunk_index: expandedChunk.chunk_index,
|
|
4188
|
-
chunk_id: expandedChunk.id,
|
|
4189
|
-
chunk_source: expandedChunk.source,
|
|
4190
|
-
chunk_metadata: expandedChunk.metadata,
|
|
4191
|
-
chunk_created_at: expandedChunk.createdAt,
|
|
4192
|
-
chunk_updated_at: expandedChunk.updatedAt,
|
|
4193
|
-
item_updated_at: chunk.item_updated_at,
|
|
4194
|
-
item_created_at: chunk.item_created_at,
|
|
4195
|
-
item_id: chunk.item_id,
|
|
4196
|
-
item_external_id: chunk.item_external_id,
|
|
4197
|
-
item_name: chunk.item_name,
|
|
4198
|
-
chunk_cosine_distance: 0,
|
|
4199
|
-
chunk_fts_rank: 0,
|
|
4200
|
-
chunk_hybrid_score: 0,
|
|
4201
|
-
context: {
|
|
4202
|
-
name: table.name.singular,
|
|
4203
|
-
id: table.id || ""
|
|
4204
|
-
}
|
|
4205
|
-
});
|
|
4206
|
-
}
|
|
4207
|
-
})
|
|
4208
|
-
);
|
|
4209
|
-
}
|
|
4210
|
-
}
|
|
4211
|
-
results = Array.from(expandedMap.values());
|
|
4212
4221
|
results = results.sort((a, b) => {
|
|
4213
4222
|
if (a.item_id !== b.item_id) {
|
|
4214
4223
|
return a.item_id.localeCompare(b.item_id);
|
|
4215
4224
|
}
|
|
4216
|
-
|
|
4217
|
-
const bIndex = Number(b.chunk_index);
|
|
4218
|
-
return aIndex - bIndex;
|
|
4225
|
+
return Number(a.chunk_index) - Number(b.chunk_index);
|
|
4219
4226
|
});
|
|
4220
4227
|
}
|
|
4221
4228
|
if (entitiesOn) {
|
|
@@ -7741,6 +7748,19 @@ function serializeError(err, depth = 0) {
|
|
|
7741
7748
|
return { message: String(err) };
|
|
7742
7749
|
}
|
|
7743
7750
|
|
|
7751
|
+
// src/exulu/turn-metadata.ts
|
|
7752
|
+
function finishTurnMetadata(opts) {
|
|
7753
|
+
const now = opts.now ?? Date.now();
|
|
7754
|
+
return {
|
|
7755
|
+
totalTokens: opts.totalUsage.totalTokens,
|
|
7756
|
+
reasoningTokens: opts.totalUsage.reasoningTokens,
|
|
7757
|
+
inputTokens: opts.totalUsage.inputTokens,
|
|
7758
|
+
outputTokens: opts.totalUsage.outputTokens,
|
|
7759
|
+
cachedInputTokens: opts.totalUsage.cachedInputTokens,
|
|
7760
|
+
durationMs: Math.max(0, now - opts.startedAt)
|
|
7761
|
+
};
|
|
7762
|
+
}
|
|
7763
|
+
|
|
7744
7764
|
// src/exulu/agent-as-tool.ts
|
|
7745
7765
|
import { z as z2 } from "zod";
|
|
7746
7766
|
|
|
@@ -7846,6 +7866,54 @@ async function resolveFreshFileUrl(url, opts) {
|
|
|
7846
7866
|
}
|
|
7847
7867
|
}
|
|
7848
7868
|
|
|
7869
|
+
// src/exulu/session-file-listing.ts
|
|
7870
|
+
var DEFAULT_MAX = 25;
|
|
7871
|
+
function formatSize(bytes) {
|
|
7872
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
7873
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
7874
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
7875
|
+
}
|
|
7876
|
+
function formatAge(then, now) {
|
|
7877
|
+
const minutes = Math.max(0, Math.round((now.getTime() - then.getTime()) / 6e4));
|
|
7878
|
+
if (minutes < 1) return "just now";
|
|
7879
|
+
if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
|
|
7880
|
+
const hours = Math.round(minutes / 60);
|
|
7881
|
+
if (hours < 48) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
7882
|
+
const days = Math.round(hours / 24);
|
|
7883
|
+
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
7884
|
+
}
|
|
7885
|
+
function describeSessionFiles(files, opts = {}) {
|
|
7886
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
7887
|
+
const max = opts.max ?? DEFAULT_MAX;
|
|
7888
|
+
const usable = files.filter((f) => f.name && !f.name.endsWith("/") && !isIgnoredArtifactPath(f.name)).sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
7889
|
+
if (usable.length === 0) return "";
|
|
7890
|
+
const shown = usable.slice(0, max);
|
|
7891
|
+
const lines = shown.map((f) => {
|
|
7892
|
+
const isNew = opts.lastTurnAt ? f.lastModified.getTime() > opts.lastTurnAt.getTime() : false;
|
|
7893
|
+
return `- ${f.name} (${formatSize(f.size)}, ${formatAge(f.lastModified, now)})${isNew ? " [NEW since your last answer]" : ""}`;
|
|
7894
|
+
});
|
|
7895
|
+
const omitted = usable.length - shown.length;
|
|
7896
|
+
if (omitted > 0) lines.push(`\u2026 and ${omitted} more file${omitted === 1 ? "" : "s"} (list them with \`ls\`).`);
|
|
7897
|
+
return "Files currently in this session (newest first):\n" + lines.join("\n") + '\nThese files are available to you. When the user refers to one of these files, to "the document" or "the attachment", or asks something only such a file can answer, read it with parse_document, view_document_page or read_session_file. Otherwise proceed as usual, e.g. with the knowledge bases.';
|
|
7898
|
+
}
|
|
7899
|
+
async function loadSessionFileListing(opts) {
|
|
7900
|
+
const uploads = opts.exuluConfig?.fileUploads;
|
|
7901
|
+
if (!uploads?.s3Bucket) return "";
|
|
7902
|
+
const prefix = sessionFilePrefix(opts.ownerId, opts.sessionID, uploads.s3prefix);
|
|
7903
|
+
try {
|
|
7904
|
+
const objects = await listS3ObjectsByPrefix(prefix, opts.exuluConfig);
|
|
7905
|
+
const files = objects.map((o) => ({
|
|
7906
|
+
name: o.key.slice(o.key.indexOf(prefix) + prefix.length),
|
|
7907
|
+
size: o.size,
|
|
7908
|
+
lastModified: new Date(o.lastModified)
|
|
7909
|
+
}));
|
|
7910
|
+
return describeSessionFiles(files, { lastTurnAt: opts.lastTurnAt });
|
|
7911
|
+
} catch (err) {
|
|
7912
|
+
console.warn(`[EXULU] could not list session files for prompt (session ${opts.sessionID}):`, err);
|
|
7913
|
+
return "";
|
|
7914
|
+
}
|
|
7915
|
+
}
|
|
7916
|
+
|
|
7849
7917
|
// src/exulu/generate-stream.ts
|
|
7850
7918
|
import {
|
|
7851
7919
|
convertToModelMessages,
|
|
@@ -8085,11 +8153,12 @@ function resolveTurnStepBudget(maxStepCount, agent) {
|
|
|
8085
8153
|
}
|
|
8086
8154
|
return DEFAULT_MAX_STEPS;
|
|
8087
8155
|
}
|
|
8156
|
+
var TOOL_INPUT_FLATTEN_CHARS = 6e3;
|
|
8088
8157
|
function flattenPart(part) {
|
|
8089
8158
|
const p = part;
|
|
8090
8159
|
if (p?.type === "text") return p.text ?? "";
|
|
8091
8160
|
if (p?.type === "tool-call") {
|
|
8092
|
-
return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0,
|
|
8161
|
+
return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, TOOL_INPUT_FLATTEN_CHARS)}`;
|
|
8093
8162
|
}
|
|
8094
8163
|
if (p?.type === "tool-result") {
|
|
8095
8164
|
const out = p.output?.value ?? p.output;
|
|
@@ -8111,7 +8180,7 @@ function flattenToolHistory(messages) {
|
|
|
8111
8180
|
return m;
|
|
8112
8181
|
});
|
|
8113
8182
|
}
|
|
8114
|
-
var FINAL_ANSWER_INSTRUCTION = `This is your last step for this turn. Answer the user's original question now, in plain text, using only the information gathered above. If you could not finish the task, tell the user you reached the maximum number of tool steps, summarize what you found and did so far, and say what remains \u2014 they can ask you to continue. Do not attempt any further tool calls. Write your answer as normal prose for the user: do not output tool-call syntax, JSON commands, or bracketed lines such as "[called tool ...]" \u2014 describe anything you did or still plan to do in plain language.`;
|
|
8183
|
+
var FINAL_ANSWER_INSTRUCTION = `This is your last step for this turn. Answer the user's original question now, in plain text, using only the information gathered above. Do not invent, estimate or "fill in" values that were not actually gathered: report only what the tools returned or what you wrote down, and name explicitly what is missing. If you could not finish the task, tell the user you reached the maximum number of tool steps, summarize what you found and did so far, and say what remains \u2014 they can ask you to continue. Do not attempt any further tool calls. Write your answer as normal prose for the user: do not output tool-call syntax, JSON commands, or bracketed lines such as "[called tool ...]" \u2014 describe anything you did or still plan to do in plain language.`;
|
|
8115
8184
|
function finalAnswerGuard(maxSteps) {
|
|
8116
8185
|
return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
|
|
8117
8186
|
toolChoice: "none",
|
|
@@ -8136,6 +8205,35 @@ function retrievalBudgetGuard(limit, agenticToolKey, allToolKeys) {
|
|
|
8136
8205
|
};
|
|
8137
8206
|
}
|
|
8138
8207
|
|
|
8208
|
+
// src/exulu/resolve-reasoning-effort.ts
|
|
8209
|
+
var REASONING_EFFORTS = ["none", "disable", "minimal", "low", "medium", "high"];
|
|
8210
|
+
function resolveReasoningEffort(agent) {
|
|
8211
|
+
const raw = agent?.reasoning_effort;
|
|
8212
|
+
if (typeof raw !== "string") return void 0;
|
|
8213
|
+
const normalized = raw.trim().toLowerCase();
|
|
8214
|
+
return REASONING_EFFORTS.includes(normalized) ? normalized : void 0;
|
|
8215
|
+
}
|
|
8216
|
+
function resolveProviderOptions(agent) {
|
|
8217
|
+
const effort = resolveReasoningEffort(agent);
|
|
8218
|
+
return {
|
|
8219
|
+
openai: { reasoningSummary: "auto" },
|
|
8220
|
+
...effort ? { litellm: { reasoningEffort: effort } } : {}
|
|
8221
|
+
};
|
|
8222
|
+
}
|
|
8223
|
+
|
|
8224
|
+
// src/exulu/stream-error.ts
|
|
8225
|
+
function onChatStreamError({ error }) {
|
|
8226
|
+
const detail = error instanceof Error ? error.message : error === void 0 ? "unknown error" : safeStringify(error);
|
|
8227
|
+
console.error("[EXULU] chat stream error.", detail);
|
|
8228
|
+
}
|
|
8229
|
+
function safeStringify(value) {
|
|
8230
|
+
try {
|
|
8231
|
+
return JSON.stringify(value) ?? String(value);
|
|
8232
|
+
} catch {
|
|
8233
|
+
return String(value);
|
|
8234
|
+
}
|
|
8235
|
+
}
|
|
8236
|
+
|
|
8139
8237
|
// src/exulu/generate-stream.ts
|
|
8140
8238
|
var processFilePartsInMessages = async (messages, offloadCtx) => {
|
|
8141
8239
|
const processedMessages = await Promise.all(
|
|
@@ -8229,6 +8327,15 @@ var saveChat = async ({
|
|
|
8229
8327
|
await mutation;
|
|
8230
8328
|
}
|
|
8231
8329
|
};
|
|
8330
|
+
var lastMessageTime = (rows) => {
|
|
8331
|
+
let latest;
|
|
8332
|
+
for (const row of rows) {
|
|
8333
|
+
if (!row.createdAt) continue;
|
|
8334
|
+
const d = new Date(row.createdAt);
|
|
8335
|
+
if (!latest || d > latest) latest = d;
|
|
8336
|
+
}
|
|
8337
|
+
return latest;
|
|
8338
|
+
};
|
|
8232
8339
|
var getAgentMessages = async ({
|
|
8233
8340
|
session,
|
|
8234
8341
|
user,
|
|
@@ -8286,6 +8393,7 @@ var generateSync = async ({
|
|
|
8286
8393
|
let project;
|
|
8287
8394
|
let sessionItems;
|
|
8288
8395
|
let sessionOwnerId;
|
|
8396
|
+
let lastTurnAt;
|
|
8289
8397
|
if (session) {
|
|
8290
8398
|
const sessionData = await getSession({ sessionID: session });
|
|
8291
8399
|
sessionItems = sessionData.session_items;
|
|
@@ -8300,6 +8408,7 @@ var generateSync = async ({
|
|
|
8300
8408
|
session,
|
|
8301
8409
|
user: user.id
|
|
8302
8410
|
});
|
|
8411
|
+
lastTurnAt = lastMessageTime(previousMessages);
|
|
8303
8412
|
const previousMessagesContent = previousMessages.map(
|
|
8304
8413
|
(message) => JSON.parse(message.content)
|
|
8305
8414
|
);
|
|
@@ -8462,6 +8571,15 @@ var generateSync = async ({
|
|
|
8462
8571
|
commands like \`node create_doc.js\`) live in the same place. These files are scoped to
|
|
8463
8572
|
this single session; they are NOT visible in other sessions, projects, or knowledge bases.
|
|
8464
8573
|
`;
|
|
8574
|
+
if (session) {
|
|
8575
|
+
const listing = await loadSessionFileListing({
|
|
8576
|
+
sessionID: session,
|
|
8577
|
+
ownerId: sessionOwnerId ?? user?.id ?? "api",
|
|
8578
|
+
exuluConfig,
|
|
8579
|
+
lastTurnAt
|
|
8580
|
+
});
|
|
8581
|
+
if (listing) system += "\n\n" + listing;
|
|
8582
|
+
}
|
|
8465
8583
|
system += `
|
|
8466
8584
|
|
|
8467
8585
|
When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
|
|
@@ -8631,6 +8749,7 @@ var generateStream = async ({
|
|
|
8631
8749
|
let project;
|
|
8632
8750
|
let sessionItems;
|
|
8633
8751
|
let sessionOwnerId;
|
|
8752
|
+
let lastTurnAt;
|
|
8634
8753
|
if (session) {
|
|
8635
8754
|
const sessionData = await getSession({ sessionID: session });
|
|
8636
8755
|
project = sessionData.project;
|
|
@@ -8646,6 +8765,7 @@ var generateStream = async ({
|
|
|
8646
8765
|
includeAllUsers: isRunSessionMetadata(sessionData.metadata)
|
|
8647
8766
|
});
|
|
8648
8767
|
previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
|
|
8768
|
+
lastTurnAt = lastMessageTime(previousMessages2);
|
|
8649
8769
|
}
|
|
8650
8770
|
const model = languageModel;
|
|
8651
8771
|
messages = await validateUIMessages({
|
|
@@ -8788,6 +8908,15 @@ ${skillsList}
|
|
|
8788
8908
|
truncation notice, e.g. tool-output-*.txt). Use the read_session_file tool with offset/limit
|
|
8789
8909
|
to page through it \u2014 do not ask the user to re-upload.
|
|
8790
8910
|
`;
|
|
8911
|
+
if (session) {
|
|
8912
|
+
const listing = await loadSessionFileListing({
|
|
8913
|
+
sessionID: session,
|
|
8914
|
+
ownerId: sessionOwnerId ?? user?.id ?? "api",
|
|
8915
|
+
exuluConfig,
|
|
8916
|
+
lastTurnAt
|
|
8917
|
+
});
|
|
8918
|
+
if (listing) system += "\n\n" + listing;
|
|
8919
|
+
}
|
|
8791
8920
|
system += `
|
|
8792
8921
|
|
|
8793
8922
|
When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
|
|
@@ -8890,18 +9019,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
8890
9019
|
// for the first step or change other parameters.
|
|
8891
9020
|
system,
|
|
8892
9021
|
maxRetries: 2,
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
}
|
|
8897
|
-
},
|
|
9022
|
+
// OpenAI reasoning summaries + the agent's optional thinking budget
|
|
9023
|
+
// (agents.reasoning_effort → LiteLLM reasoning_effort).
|
|
9024
|
+
providerOptions: resolveProviderOptions(agent),
|
|
8898
9025
|
tools,
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
throw new Error(
|
|
8902
|
-
`Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
|
|
8903
|
-
);
|
|
8904
|
-
},
|
|
9026
|
+
// Log only — throwing here crashed the process (see stream-error.ts).
|
|
9027
|
+
onError: onChatStreamError,
|
|
8905
9028
|
// todo allow configuring the step budget per skill
|
|
8906
9029
|
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
|
|
8907
9030
|
stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
|
|
@@ -8913,6 +9036,44 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
8913
9036
|
};
|
|
8914
9037
|
};
|
|
8915
9038
|
|
|
9039
|
+
// src/utils/agent-glossary.ts
|
|
9040
|
+
function extractGlossaryFromAgentTools(tools) {
|
|
9041
|
+
if (!Array.isArray(tools)) return [];
|
|
9042
|
+
const searchTool = tools.find((t) => t?.id === "agentic_context_search");
|
|
9043
|
+
const entry = searchTool?.config?.find((c) => c?.name === "vocabulary");
|
|
9044
|
+
if (entry === void 0 || entry.variable === void 0 || entry.variable === null || entry.variable === "") {
|
|
9045
|
+
return [];
|
|
9046
|
+
}
|
|
9047
|
+
try {
|
|
9048
|
+
const parsed = typeof entry.variable === "string" ? JSON.parse(entry.variable) : entry.variable;
|
|
9049
|
+
const glossary = parsed?.glossary;
|
|
9050
|
+
if (!Array.isArray(glossary)) return [];
|
|
9051
|
+
return glossary.filter(
|
|
9052
|
+
(g) => !!g && typeof g.term === "string" && typeof g.meaning === "string" && g.term.trim().length > 0
|
|
9053
|
+
);
|
|
9054
|
+
} catch (err) {
|
|
9055
|
+
console.warn(
|
|
9056
|
+
"[EXULU] Failed to parse the agentic_context_search vocabulary config while building agent instructions.",
|
|
9057
|
+
err
|
|
9058
|
+
);
|
|
9059
|
+
return [];
|
|
9060
|
+
}
|
|
9061
|
+
}
|
|
9062
|
+
function formatGlossaryBlock(glossary) {
|
|
9063
|
+
if (!glossary.length) return "";
|
|
9064
|
+
const lines = glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n");
|
|
9065
|
+
return `The organization's documents and internal terminology use the following abbreviations/terms:
|
|
9066
|
+
${lines}`;
|
|
9067
|
+
}
|
|
9068
|
+
function withGlossary(baseInstructions, tools) {
|
|
9069
|
+
const instructions = baseInstructions ?? "";
|
|
9070
|
+
const block = formatGlossaryBlock(extractGlossaryFromAgentTools(tools));
|
|
9071
|
+
if (!block) return instructions;
|
|
9072
|
+
return instructions ? `${block}
|
|
9073
|
+
|
|
9074
|
+
${instructions}` : block;
|
|
9075
|
+
}
|
|
9076
|
+
|
|
8916
9077
|
// src/exulu/agent-as-tool.ts
|
|
8917
9078
|
var createAgentTool = async (instance, contexts) => {
|
|
8918
9079
|
const agent = await exuluApp.get().agent(instance);
|
|
@@ -8971,7 +9132,7 @@ var createAgentTool = async (instance, contexts) => {
|
|
|
8971
9132
|
const response = await generateSync({
|
|
8972
9133
|
agent,
|
|
8973
9134
|
contexts,
|
|
8974
|
-
instructions: agent.instructions,
|
|
9135
|
+
instructions: withGlossary(agent.instructions, agent.tools),
|
|
8975
9136
|
prompt: "The user has asked the following question: " + prompt + " and the following information is available: " + information,
|
|
8976
9137
|
languageModel: resolved.languageModel,
|
|
8977
9138
|
user,
|
|
@@ -10744,13 +10905,7 @@ var processUiMessagesFlow = async ({
|
|
|
10744
10905
|
messageMetadata: ({ part }) => {
|
|
10745
10906
|
console.log("[EXULU] part", part.type);
|
|
10746
10907
|
if (part.type === "finish") {
|
|
10747
|
-
return {
|
|
10748
|
-
totalTokens: part.totalUsage.totalTokens,
|
|
10749
|
-
reasoningTokens: part.totalUsage.reasoningTokens,
|
|
10750
|
-
inputTokens: part.totalUsage.inputTokens,
|
|
10751
|
-
outputTokens: part.totalUsage.outputTokens,
|
|
10752
|
-
cachedInputTokens: part.totalUsage.cachedInputTokens
|
|
10753
|
-
};
|
|
10908
|
+
return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: startTime });
|
|
10754
10909
|
}
|
|
10755
10910
|
return void 0;
|
|
10756
10911
|
},
|
|
@@ -11964,7 +12119,7 @@ var recallService = {
|
|
|
11964
12119
|
);
|
|
11965
12120
|
const { text } = await generateText4({
|
|
11966
12121
|
model: resolved.languageModel,
|
|
11967
|
-
system: agent.instructions || void 0,
|
|
12122
|
+
system: withGlossary(agent.instructions, agent.tools) || void 0,
|
|
11968
12123
|
prompt: `${prompt.content}
|
|
11969
12124
|
|
|
11970
12125
|
---
|
|
@@ -16681,10 +16836,14 @@ var createExpressRoutes = async (app, tools, contexts, config, evals, tracer) =>
|
|
|
16681
16836
|
}
|
|
16682
16837
|
const approvedTools = req.body.approvedTools ? typeof req.body.approvedTools === "string" ? JSON.parse(req.body.approvedTools) : req.body.approvedTools : [];
|
|
16683
16838
|
const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
|
|
16684
|
-
const instructions =
|
|
16839
|
+
const instructions = withGlossary(
|
|
16840
|
+
customInstructions ? `${agent.instructions}
|
|
16685
16841
|
|
|
16686
|
-
${customInstructions}` : agent.instructions
|
|
16842
|
+
${customInstructions}` : agent.instructions,
|
|
16843
|
+
agent.tools
|
|
16844
|
+
);
|
|
16687
16845
|
if (headers.session) markStreamActive(headers.session);
|
|
16846
|
+
const turnStartedAt = Date.now();
|
|
16688
16847
|
let result;
|
|
16689
16848
|
try {
|
|
16690
16849
|
result = await generateStream({
|
|
@@ -16726,13 +16885,7 @@ ${customInstructions}` : agent.instructions;
|
|
|
16726
16885
|
};
|
|
16727
16886
|
}
|
|
16728
16887
|
if (part.type === "finish") {
|
|
16729
|
-
return {
|
|
16730
|
-
totalTokens: part.totalUsage.totalTokens,
|
|
16731
|
-
reasoningTokens: part.totalUsage.reasoningTokens,
|
|
16732
|
-
inputTokens: part.totalUsage.inputTokens,
|
|
16733
|
-
outputTokens: part.totalUsage.outputTokens,
|
|
16734
|
-
cachedInputTokens: part.totalUsage.cachedInputTokens
|
|
16735
|
-
};
|
|
16888
|
+
return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: turnStartedAt });
|
|
16736
16889
|
}
|
|
16737
16890
|
return void 0;
|
|
16738
16891
|
},
|
|
@@ -16824,9 +16977,12 @@ ${customInstructions}` : agent.instructions;
|
|
|
16824
16977
|
return;
|
|
16825
16978
|
} else {
|
|
16826
16979
|
const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
|
|
16827
|
-
const instructions =
|
|
16980
|
+
const instructions = withGlossary(
|
|
16981
|
+
customInstructions ? `${agent.instructions}
|
|
16828
16982
|
|
|
16829
|
-
${customInstructions}` : agent.instructions
|
|
16983
|
+
${customInstructions}` : agent.instructions,
|
|
16984
|
+
agent.tools
|
|
16985
|
+
);
|
|
16830
16986
|
let response;
|
|
16831
16987
|
try {
|
|
16832
16988
|
response = await generateSync({
|
|
@@ -24151,7 +24307,7 @@ function reconstructTableHeaders(document, validationResults, verbose = false) {
|
|
|
24151
24307
|
}
|
|
24152
24308
|
}
|
|
24153
24309
|
async function validateWithVLM(document, model, verbose = false, concurrency = 10) {
|
|
24154
|
-
console.log(`[EXULU] Starting VLM validation for
|
|
24310
|
+
console.log(`[EXULU] Starting VLM validation for processor output, ${document.length} pages...`);
|
|
24155
24311
|
console.log(`[EXULU] Concurrency limit: ${concurrency}`);
|
|
24156
24312
|
const limit = pLimit(concurrency);
|
|
24157
24313
|
const validationResults = /* @__PURE__ */ new Map();
|
|
@@ -24286,48 +24442,7 @@ async function processDocument(filePath, fileType, buffer, tempDir, config, verb
|
|
|
24286
24442
|
async function processPdf(buffer, paths, config, verbose = false) {
|
|
24287
24443
|
try {
|
|
24288
24444
|
let json = [];
|
|
24289
|
-
if (config?.processor.name === "
|
|
24290
|
-
console.log(`[EXULU] Validating Python environment...`);
|
|
24291
|
-
const validation = await validatePythonEnvironment(void 0, true);
|
|
24292
|
-
if (!validation.valid) {
|
|
24293
|
-
console.log(`[EXULU] Python environment not ready, setting up automatically...`);
|
|
24294
|
-
console.log(`[EXULU] Reason: ${validation.message}`);
|
|
24295
|
-
const setupResult = await setupPythonEnvironment({
|
|
24296
|
-
verbose: true,
|
|
24297
|
-
force: false
|
|
24298
|
-
// Only setup if not already done
|
|
24299
|
-
});
|
|
24300
|
-
if (!setupResult.success) {
|
|
24301
|
-
throw new Error(`Failed to setup Python environment: ${setupResult.message}
|
|
24302
|
-
|
|
24303
|
-
${setupResult.output || ""}`);
|
|
24304
|
-
}
|
|
24305
|
-
console.log(`[EXULU] Python environment setup completed successfully`);
|
|
24306
|
-
} else {
|
|
24307
|
-
console.log(`[EXULU] Python environment is valid`);
|
|
24308
|
-
}
|
|
24309
|
-
console.log(`[EXULU] Processing document with document_to_markdown.py`);
|
|
24310
|
-
const result = await executePythonScript({
|
|
24311
|
-
scriptPath: "ee/python/documents/processing/document_to_markdown.py",
|
|
24312
|
-
args: [
|
|
24313
|
-
paths.source,
|
|
24314
|
-
"-o",
|
|
24315
|
-
paths.json,
|
|
24316
|
-
"--images-dir",
|
|
24317
|
-
paths.images
|
|
24318
|
-
],
|
|
24319
|
-
timeout: 30 * 60 * 1e3
|
|
24320
|
-
// 30 minutes for large documents
|
|
24321
|
-
});
|
|
24322
|
-
if (result.stderr) {
|
|
24323
|
-
console.log("Processing info:", result.stderr.trim());
|
|
24324
|
-
}
|
|
24325
|
-
if (!result.success) {
|
|
24326
|
-
throw new Error(`Document processing failed: ${result.stderr}`);
|
|
24327
|
-
}
|
|
24328
|
-
const jsonContent = await fs3.promises.readFile(paths.json, "utf-8");
|
|
24329
|
-
json = JSON.parse(jsonContent);
|
|
24330
|
-
} else if (config?.processor.name === "officeparser") {
|
|
24445
|
+
if (config?.processor.name === "officeparser") {
|
|
24331
24446
|
const text = await parseOfficeAsync2(buffer, {
|
|
24332
24447
|
outputErrorToConsole: false,
|
|
24333
24448
|
newlineDelimiter: "\n"
|
|
@@ -24418,14 +24533,16 @@ stderr: ${splitResult.stderr.slice(-1e3)}`
|
|
|
24418
24533
|
image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
|
|
24419
24534
|
}));
|
|
24420
24535
|
fs3.writeFileSync(paths.json, JSON.stringify(json, null, 2));
|
|
24536
|
+
} else {
|
|
24537
|
+
const configured = String(config?.processor?.name ?? "");
|
|
24538
|
+
throw new Error(
|
|
24539
|
+
configured === "" ? "[EXULU] No document processor configured. Set processor.name to one of: mistral, liteparse, officeparser." : `[EXULU] Unknown document processor "${configured}". Supported processors are: mistral, liteparse, officeparser.` + (configured === "docling" ? ' The "docling" processor was removed: it depended on PyMuPDF, which is AGPL-licensed. Use "mistral" for PDF OCR.' : "")
|
|
24540
|
+
);
|
|
24421
24541
|
}
|
|
24422
24542
|
console.log(`[EXULU]
|
|
24423
24543
|
\u2713 Document processing completed successfully`);
|
|
24424
24544
|
console.log(`[EXULU] Total pages: ${json.length}`);
|
|
24425
24545
|
console.log(`[EXULU] Output file: ${paths.json}`);
|
|
24426
|
-
if (config?.vlm?.model) {
|
|
24427
|
-
console.error("[EXULU] VLM validation is only supported when docling is enabled, skipping validation.");
|
|
24428
|
-
}
|
|
24429
24546
|
const vlmModel = config?.vlm?.model ? await resolveVlmModel(config) : void 0;
|
|
24430
24547
|
if (vlmModel && json.length > 0) {
|
|
24431
24548
|
json = await validateWithVLM(
|
|
@@ -24543,9 +24660,6 @@ async function documentProcessor({
|
|
|
24543
24660
|
} = await loadFile(file, name, tempDir);
|
|
24544
24661
|
let supportedTypes = [];
|
|
24545
24662
|
switch (config?.processor.name) {
|
|
24546
|
-
case "docling":
|
|
24547
|
-
supportedTypes = ["pdf", "docx", "doc", "txt", "md", "jpg", "jpeg", "png", "gif", "webp"];
|
|
24548
|
-
break;
|
|
24549
24663
|
case "officeparser":
|
|
24550
24664
|
supportedTypes = ["docx", "pptx", "xlsx", "odt", "odp", "ods", "pdf", "rtf", "csv", "md", "html"];
|
|
24551
24665
|
break;
|