@exulu/backend 1.54.0 → 1.55.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/index.js CHANGED
@@ -488,7 +488,7 @@ var STATISTICS_TYPE_ENUM = {
488
488
  };
489
489
 
490
490
  // src/exulu/routes.ts
491
- import express from "express";
491
+ import express2 from "express";
492
492
  import { ApolloServer } from "@apollo/server";
493
493
  import cors from "cors";
494
494
  import "reflect-metadata";
@@ -747,7 +747,7 @@ var JOB_STATUS_ENUM = {
747
747
  };
748
748
 
749
749
  // ee/agentic-retrieval/v3/index.ts
750
- import { z as z10 } from "zod";
750
+ import { z as z9 } from "zod";
751
751
  import { createBashTool } from "bash-tool";
752
752
 
753
753
  // src/exulu/tool.ts
@@ -981,10 +981,25 @@ var createSessionItemsRetrievalTool = async ({
981
981
  return sessionItemsRetrievalTool;
982
982
  };
983
983
 
984
+ // ee/agentic-retrieval/v3/session-tools-registry.ts
985
+ var registry = /* @__PURE__ */ new Map();
986
+ function registerSessionTools(sessionId, tools) {
987
+ const existing = registry.get(sessionId) ?? /* @__PURE__ */ new Map();
988
+ for (const [name, toolDef] of Object.entries(tools)) {
989
+ existing.set(name, toolDef);
990
+ }
991
+ registry.set(sessionId, existing);
992
+ }
993
+ function getSessionTools(sessionId) {
994
+ const toolMap = registry.get(sessionId);
995
+ if (!toolMap || toolMap.size === 0) return {};
996
+ return Object.fromEntries(toolMap.entries());
997
+ }
998
+
984
999
  // src/utils/sanitize-tool-name.ts
985
1000
  function sanitizeToolName(name) {
986
1001
  if (typeof name !== "string") return "";
987
- let sanitized = name.replace(/[^a-zA-Z0-9_.\:-]+/g, "_");
1002
+ let sanitized = name.replace(/[^a-zA-Z0-9_\:-]+/g, "_");
988
1003
  if (sanitized.length > 0 && !/^[a-zA-Z_]/.test(sanitized)) {
989
1004
  sanitized = "_" + sanitized;
990
1005
  }
@@ -1006,7 +1021,6 @@ import { randomUUID } from "crypto";
1006
1021
 
1007
1022
  // src/templates/tools/memory-tool.ts
1008
1023
  import { z as z3 } from "zod";
1009
- import "fs";
1010
1024
  var createNewMemoryItemTool = (agent, context) => {
1011
1025
  const fields = {
1012
1026
  name: z3.string().describe("The name of the item to create"),
@@ -1144,9 +1158,9 @@ var getMimeType = (type) => {
1144
1158
  return "";
1145
1159
  }
1146
1160
  };
1147
- var hydrateVariables = async (tool6) => {
1161
+ var hydrateVariables = async (tool5) => {
1148
1162
  const { db: db2 } = await postgresClient();
1149
- const promises2 = tool6.config.map(async (toolConfig) => {
1163
+ const promises2 = tool5.config.map(async (toolConfig) => {
1150
1164
  if (!toolConfig.variable) {
1151
1165
  return toolConfig;
1152
1166
  }
@@ -1177,9 +1191,9 @@ var hydrateVariables = async (tool6) => {
1177
1191
  return toolConfig;
1178
1192
  });
1179
1193
  await Promise.all(promises2);
1180
- return tool6;
1194
+ return tool5;
1181
1195
  };
1182
- var convertExuluToolsToAiSdkTools = async (currentTools, approvedTools, allExuluTools, configs, providerapikey, contexts, rerankers, user, exuluConfig, sessionID, req, project, items, model, agent) => {
1196
+ var convertExuluToolsToAiSdkTools = async (currentTools, approvedTools, allExuluTools, configs, providerapikey, contexts, rerankers, user, exuluConfig, sessionID, req, project, sessionItems, model, agent) => {
1183
1197
  if (!currentTools) return {};
1184
1198
  if (!allExuluTools) {
1185
1199
  allExuluTools = [];
@@ -1214,13 +1228,13 @@ var convertExuluToolsToAiSdkTools = async (currentTools, approvedTools, allExulu
1214
1228
  currentTools.push(createNewMemoryTool);
1215
1229
  }
1216
1230
  }
1217
- console.log("[EXULU] Convert tools array to object, session items", items);
1218
- if (items) {
1231
+ console.log("[EXULU] Convert tools array to object, session items", sessionItems);
1232
+ if (sessionItems) {
1219
1233
  const sessionItemsRetrievalTool = await createSessionItemsRetrievalTool({
1220
1234
  user,
1221
1235
  role: user?.role?.id,
1222
1236
  contexts,
1223
- items
1237
+ items: sessionItems
1224
1238
  });
1225
1239
  if (sessionItemsRetrievalTool) {
1226
1240
  currentTools.push(sessionItemsRetrievalTool);
@@ -1234,10 +1248,11 @@ var convertExuluToolsToAiSdkTools = async (currentTools, approvedTools, allExulu
1234
1248
  rerankers: rerankers || [],
1235
1249
  user,
1236
1250
  role: user?.role?.id,
1237
- model
1251
+ model,
1252
+ preselectedItemIds: sessionItems
1238
1253
  });
1239
1254
  if (agenticSearchTool) {
1240
- const index = currentTools.findIndex((tool6) => tool6.id === "agentic_context_search");
1255
+ const index = currentTools.findIndex((tool5) => tool5.id === "agentic_context_search");
1241
1256
  if (index !== -1) {
1242
1257
  currentTools[index] = {
1243
1258
  ...currentTools[index],
@@ -1247,21 +1262,26 @@ var convertExuluToolsToAiSdkTools = async (currentTools, approvedTools, allExulu
1247
1262
  }
1248
1263
  }
1249
1264
  } else {
1250
- const agenticSearchTool = currentTools.find((tool6) => tool6.id === "agentic_context_search");
1265
+ const agenticSearchTool = currentTools.find((tool5) => tool5.id === "agentic_context_search");
1251
1266
  if (agenticSearchTool) {
1252
1267
  currentTools.splice(currentTools.indexOf(agenticSearchTool), 1);
1253
1268
  }
1254
1269
  }
1255
- const sanitizedTools = currentTools ? currentTools.map((tool6) => ({
1256
- ...tool6,
1257
- name: sanitizeToolName(tool6.name)
1270
+ const sanitizedTools = currentTools ? currentTools.map((tool5) => ({
1271
+ ...tool5,
1272
+ name: sanitizeToolName(tool5.name)
1258
1273
  })) : [];
1259
1274
  console.log(
1260
1275
  "[EXULU] Sanitized tools",
1261
1276
  sanitizedTools.map((x) => x.name + " (" + x.id + ")")
1262
1277
  );
1263
1278
  console.log("[EXULU] Approved tools", approvedTools);
1279
+ const sessionDynamicTools = sessionID ? Object.entries(getSessionTools(sessionID)).reduce((acc, [name, t]) => {
1280
+ acc[name] = { ...t, needsApproval: false };
1281
+ return acc;
1282
+ }, {}) : {};
1264
1283
  return {
1284
+ ...sessionDynamicTools,
1265
1285
  ...sanitizedTools?.reduce((prev, cur) => {
1266
1286
  let toolVariableConfig = configs?.find((config) => config.id === cur.id);
1267
1287
  const userDefinedConfigDescription = toolVariableConfig?.config.find(
@@ -1463,7 +1483,7 @@ var ExuluTool = class {
1463
1483
  });
1464
1484
  }
1465
1485
  execute = async ({
1466
- agent: agentId,
1486
+ agent: agentId2,
1467
1487
  config,
1468
1488
  user,
1469
1489
  inputs,
@@ -1471,14 +1491,14 @@ var ExuluTool = class {
1471
1491
  items
1472
1492
  }) => {
1473
1493
  console.log("[EXULU] Calling tool execute directly", {
1474
- agentId,
1494
+ agentId: agentId2,
1475
1495
  config,
1476
1496
  user,
1477
1497
  inputs,
1478
1498
  project,
1479
1499
  items
1480
1500
  });
1481
- const agent = await exuluApp.get().agent(agentId);
1501
+ const agent = await exuluApp.get().agent(agentId2);
1482
1502
  if (!agent) {
1483
1503
  throw new Error("Agent not found.");
1484
1504
  }
@@ -1521,8 +1541,8 @@ var ExuluTool = class {
1521
1541
  void 0,
1522
1542
  agent
1523
1543
  );
1524
- const tool6 = tools[sanitizeName(this.name)] || tools[this.name] || tools[this.id];
1525
- if (!tool6?.execute) {
1544
+ const tool5 = tools[sanitizeName(this.name)] || tools[this.name] || tools[this.id];
1545
+ if (!tool5?.execute) {
1526
1546
  throw new Error("Tool " + sanitizeName(this.name) + " not found in " + JSON.stringify(tools));
1527
1547
  }
1528
1548
  console.log("[EXULU] Tool found", this.name);
@@ -1532,7 +1552,7 @@ var ExuluTool = class {
1532
1552
  toolCallId,
1533
1553
  messages: []
1534
1554
  });
1535
- const generator = tool6.execute(inputs, {
1555
+ const generator = tool5.execute(inputs, {
1536
1556
  toolCallId,
1537
1557
  messages: []
1538
1558
  });
@@ -1556,6 +1576,7 @@ import {
1556
1576
  S3Client as S3Client2,
1557
1577
  AbortMultipartUploadCommand,
1558
1578
  CompleteMultipartUploadCommand,
1579
+ CopyObjectCommand,
1559
1580
  CreateMultipartUploadCommand,
1560
1581
  GetObjectCommand,
1561
1582
  ListPartsCommand,
@@ -1674,7 +1695,7 @@ var uploadFile = async (file, fileName, config, options = {}, user, customBucket
1674
1695
  if (error.name === "SignatureDoesNotMatch" || error.name === "InvalidAccessKeyId" || error.name === "AccessDenied") {
1675
1696
  if (attempt < maxRetries) {
1676
1697
  const backoffMs = Math.pow(2, attempt) * 1e3;
1677
- await new Promise((resolve4) => setTimeout(resolve4, backoffMs));
1698
+ await new Promise((resolve3) => setTimeout(resolve3, backoffMs));
1678
1699
  s3Client2 = void 0;
1679
1700
  getS3Client(config);
1680
1701
  continue;
@@ -1689,6 +1710,87 @@ var uploadFile = async (file, fileName, config, options = {}, user, customBucket
1689
1710
  }
1690
1711
  return addBucketPrefixToKey(key, customBucket || defaultBucket);
1691
1712
  };
1713
+ var listS3ObjectsByPrefix = async (prefix, config) => {
1714
+ if (!config.fileUploads) {
1715
+ throw new Error("File uploads are not configured");
1716
+ }
1717
+ const client2 = getS3Client(config);
1718
+ const bucket = config.fileUploads.s3Bucket;
1719
+ const fullPrefix = addGeneralPrefixToKey(prefix, config);
1720
+ const results = [];
1721
+ let continuationToken;
1722
+ do {
1723
+ const command = new ListObjectsV2Command({
1724
+ Bucket: bucket,
1725
+ Prefix: fullPrefix,
1726
+ ...continuationToken && { ContinuationToken: continuationToken }
1727
+ });
1728
+ const response = await client2.send(command);
1729
+ for (const obj of response.Contents ?? []) {
1730
+ if (obj.Key && obj.Size !== void 0 && obj.LastModified) {
1731
+ results.push({
1732
+ key: obj.Key,
1733
+ size: obj.Size,
1734
+ lastModified: obj.LastModified
1735
+ });
1736
+ }
1737
+ }
1738
+ continuationToken = response.IsTruncated ? response.NextContinuationToken : void 0;
1739
+ } while (continuationToken);
1740
+ return results;
1741
+ };
1742
+ var copyS3Object = async (sourceKey, destKey, config) => {
1743
+ if (!config.fileUploads) {
1744
+ throw new Error("File uploads are not configured");
1745
+ }
1746
+ const client2 = getS3Client(config);
1747
+ const bucket = config.fileUploads.s3Bucket;
1748
+ const command = new CopyObjectCommand({
1749
+ Bucket: bucket,
1750
+ CopySource: `${bucket}/${sourceKey}`,
1751
+ Key: destKey
1752
+ });
1753
+ await client2.send(command);
1754
+ };
1755
+ var getS3ObjectContent = async (key, config) => {
1756
+ if (!config.fileUploads) {
1757
+ throw new Error("File uploads are not configured");
1758
+ }
1759
+ const client2 = getS3Client(config);
1760
+ const bucket = config.fileUploads.s3Bucket;
1761
+ const command = new GetObjectCommand({ Bucket: bucket, Key: key });
1762
+ const response = await client2.send(command);
1763
+ if (!response.Body) {
1764
+ throw new Error(`Empty body for S3 key: ${key}`);
1765
+ }
1766
+ return response.Body.transformToString("utf-8");
1767
+ };
1768
+ var deleteS3Object = async (key, config) => {
1769
+ if (!config.fileUploads) {
1770
+ throw new Error("File uploads are not configured");
1771
+ }
1772
+ const client2 = getS3Client(config);
1773
+ const bucket = config.fileUploads.s3Bucket;
1774
+ const command = new DeleteObjectCommand({ Bucket: bucket, Key: key });
1775
+ await client2.send(command);
1776
+ };
1777
+ var getS3SignedUploadUrl = async (key, contentType, config) => {
1778
+ if (!config.fileUploads) {
1779
+ throw new Error("File uploads are not configured");
1780
+ }
1781
+ const client2 = getS3Client(config);
1782
+ const bucket = config.fileUploads.s3Bucket;
1783
+ const url = await getSignedUrl(
1784
+ client2,
1785
+ new PutObjectCommand2({
1786
+ Bucket: bucket,
1787
+ Key: key,
1788
+ ContentType: contentType
1789
+ }),
1790
+ { expiresIn }
1791
+ );
1792
+ return url;
1793
+ };
1692
1794
  var createUppyRoutes = async (app, config) => {
1693
1795
  if (!config.fileUploads) {
1694
1796
  throw new Error("File uploads are not configured");
@@ -2947,6 +3049,53 @@ var agentSessionsSchema = {
2947
3049
  }
2948
3050
  ]
2949
3051
  };
3052
+ var skillsSchema = {
3053
+ type: "skills",
3054
+ name: {
3055
+ plural: "skills",
3056
+ singular: "skill"
3057
+ },
3058
+ RBAC: true,
3059
+ fields: [
3060
+ {
3061
+ name: "name",
3062
+ type: "text",
3063
+ index: true,
3064
+ unique: true
3065
+ },
3066
+ {
3067
+ name: "description",
3068
+ type: "text"
3069
+ },
3070
+ {
3071
+ name: "s3folder",
3072
+ type: "text"
3073
+ },
3074
+ {
3075
+ name: "tags",
3076
+ type: "json"
3077
+ },
3078
+ {
3079
+ name: "usage_count",
3080
+ type: "number",
3081
+ default: 0
3082
+ },
3083
+ {
3084
+ name: "favorite_count",
3085
+ type: "number",
3086
+ default: 0
3087
+ },
3088
+ {
3089
+ name: "history",
3090
+ type: "json"
3091
+ },
3092
+ {
3093
+ name: "current_version",
3094
+ type: "number",
3095
+ default: 1
3096
+ }
3097
+ ]
3098
+ };
2950
3099
  var variablesSchema = {
2951
3100
  type: "variables",
2952
3101
  name: {
@@ -3065,6 +3214,10 @@ var agentsSchema = {
3065
3214
  name: "tools",
3066
3215
  type: "json"
3067
3216
  },
3217
+ {
3218
+ name: "skills",
3219
+ type: "json"
3220
+ },
3068
3221
  {
3069
3222
  name: "animation_idle",
3070
3223
  type: "text"
@@ -3324,6 +3477,7 @@ var coreSchemas = {
3324
3477
  agentSessionsSchema: () => addCoreFields(agentSessionsSchema),
3325
3478
  projectsSchema: () => addCoreFields(projectsSchema),
3326
3479
  usersSchema: () => addCoreFields(usersSchema),
3480
+ skillsSchema: () => addCoreFields(skillsSchema),
3327
3481
  statisticsSchema: () => addCoreFields(statisticsSchema),
3328
3482
  variablesSchema: () => addCoreFields(variablesSchema),
3329
3483
  platformConfigurationsSchema: () => addCoreFields(platformConfigurationsSchema),
@@ -3945,9 +4099,6 @@ var mapType = (t, type, name, defaultValue, unique) => {
3945
4099
  throw new Error("Invalid field type for database: " + type);
3946
4100
  };
3947
4101
 
3948
- // src/exulu/context.ts
3949
- import { z as z5 } from "zod";
3950
-
3951
4102
  // ee/queues/decorator.ts
3952
4103
  import "bullmq";
3953
4104
  import { v4 as uuidv4 } from "uuid";
@@ -4664,69 +4815,6 @@ var ExuluContext2 = class {
4664
4815
  `);
4665
4816
  return;
4666
4817
  };
4667
- // Exports the context as a tool that can be used by an agent
4668
- tool = () => {
4669
- if (this.configuration.enableAsTool === false) {
4670
- return null;
4671
- }
4672
- return new ExuluTool({
4673
- id: this.id,
4674
- name: `${this.name}_context_search`,
4675
- type: "context",
4676
- category: "contexts",
4677
- needsApproval: true,
4678
- // todo make configurable
4679
- inputSchema: z5.object({
4680
- query: z5.string().describe("The original question that the user asked"),
4681
- keywords: z5.array(z5.string()).describe(
4682
- "The keywords that are relevant to the user's question, for example names of specific products, systems or parts, IDs, etc."
4683
- ),
4684
- method: z5.enum(["keyword", "semantic", "hybrid"]).default("hybrid").describe(
4685
- "Search method: 'hybrid' (best for most queries - combines semantic understanding with exact term matching), 'keyword' (best for exact terms, technical names, IDs, or specific phrases), 'semantic' (best for conceptual queries where synonyms and paraphrasing matter)"
4686
- )
4687
- }),
4688
- config: [],
4689
- description: `Gets information from the context called: ${this.name}. The context description is: ${this.description}.`,
4690
- execute: async ({ query, keywords, user, role, method }) => {
4691
- const { db: db2 } = await postgresClient();
4692
- const result = await vectorSearch({
4693
- page: 1,
4694
- limit: this.configuration.maxRetrievalResults ?? 10,
4695
- query,
4696
- keywords,
4697
- itemFilters: [],
4698
- chunkFilters: [],
4699
- user,
4700
- role,
4701
- method: method === "hybrid" ? "hybridSearch" : method === "keyword" ? "tsvector" : "cosineDistance",
4702
- context: this,
4703
- db: db2,
4704
- sort: void 0,
4705
- trigger: "agent"
4706
- });
4707
- await updateStatistic({
4708
- name: "count",
4709
- label: this.name,
4710
- type: STATISTICS_TYPE_ENUM.TOOL_CALL,
4711
- trigger: "tool",
4712
- count: 1,
4713
- user: user?.id,
4714
- role: user?.role?.id
4715
- });
4716
- return {
4717
- result: JSON.stringify(
4718
- result.chunks.map((chunk) => ({
4719
- ...chunk,
4720
- context: {
4721
- name: this.name,
4722
- id: this.id
4723
- }
4724
- }))
4725
- )
4726
- };
4727
- }
4728
- });
4729
- };
4730
4818
  };
4731
4819
 
4732
4820
  // ee/agentic-retrieval/v3/context-sampler.ts
@@ -4771,7 +4859,27 @@ var ContextSampler = class {
4771
4859
 
4772
4860
  // ee/agentic-retrieval/v3/classifier.ts
4773
4861
  import { generateText, Output } from "ai";
4774
- import { z as z6 } from "zod";
4862
+ import { z as z5 } from "zod";
4863
+
4864
+ // src/utils/with-retry.ts
4865
+ async function withRetry(generateFn, maxRetries = 3) {
4866
+ let lastError;
4867
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
4868
+ try {
4869
+ return await generateFn();
4870
+ } catch (error) {
4871
+ lastError = error;
4872
+ console.error(`[EXULU] generateText attempt ${attempt} failed:`, error);
4873
+ if (attempt === maxRetries) {
4874
+ throw error;
4875
+ }
4876
+ await new Promise((resolve3) => setTimeout(resolve3, Math.pow(2, attempt) * 1e3));
4877
+ }
4878
+ }
4879
+ throw lastError;
4880
+ }
4881
+
4882
+ // ee/agentic-retrieval/v3/classifier.ts
4775
4883
  async function classifyQuery(query, contexts, samples, model) {
4776
4884
  const contextDescriptions = contexts.map((ctx) => {
4777
4885
  const sample = samples.find((s) => s.contextId === ctx.id);
@@ -4782,46 +4890,49 @@ async function classifyQuery(query, contexts, samples, model) {
4782
4890
  Description: ${ctx.description}
4783
4891
  Fields: ${fieldList}${exampleStr}`;
4784
4892
  }).join("\n\n");
4785
- const result = await generateText({
4786
- model,
4787
- temperature: 0,
4788
- output: Output.object({
4789
- schema: z6.object({
4790
- queryType: z6.enum(["aggregate", "list", "targeted", "exploratory"]).describe(
4791
- "aggregate: ONLY use when the user explicitly asks to COUNT how many documents/items/tickets exist in the knowledge base (e.g. 'how many documents about X?', 'total number of tickets'). NEVER use for: real-world statistics stored in a document, intent statements, how-to questions, error/fault descriptions, configuration questions, or any query that does not explicitly ask for a count of knowledge base entries. When in doubt, choose targeted. list: user wants to enumerate matching items/documents (show me all, list documents about). targeted: use for almost everything \u2014 specific fact, answer, configuration, how-to, error/fault, feature/behavior question. Also use for intent statements and short commands describing a desired state (phrases that state what the user wants to do or achieve, even without an explicit question word). Real-world statistics stored in documents also go here. When in doubt, choose targeted over aggregate or exploratory. exploratory: only for broad conceptual questions needing multi-source synthesis (what is the process for Z, explain how X works, general overview of topic Y)."
4792
- ),
4793
- language: z6.string().describe("ISO 639-3 language code of the query (e.g. eng, deu, fra)"),
4794
- suggestedContextIds: z6.array(z6.string()).describe(
4795
- "IDs of knowledge bases most likely to contain the answer. Return empty array to search all contexts."
4796
- )
4797
- })
4798
- }),
4799
- toolChoice: "none",
4800
- system: `You are a query classifier for a multi-knowledge-base retrieval system.
4801
- Classify the query and identify which knowledge bases are most relevant.
4802
-
4803
- Available knowledge bases:
4804
- ${contextDescriptions}
4805
-
4806
- Guidelines for queryType:
4807
- - Use "aggregate" ONLY when the query contains explicit counting language (e.g., "how many", "count", "total number", "wie viele"). Short statements, commands, or phrases without a question word are NEVER aggregate \u2014 classify them as targeted.
4808
- - When in doubt between aggregate and targeted: always choose targeted.
4809
-
4810
- Guidelines for suggestedContextIds:
4811
- - Be conservative: only suggest contexts that are genuinely likely to contain the answer.
4812
- Aim for 2\u20133 focused suggestions rather than listing everything.
4813
- - Use each knowledge base's name and description (shown above) to judge relevance.
4814
- - Return an empty array only if you truly cannot determine which contexts are relevant.`,
4815
- prompt: `Query: ${query}`
4816
- });
4817
- return result.output;
4893
+ const result = await withRetry(async () => {
4894
+ const result2 = await generateText({
4895
+ model,
4896
+ temperature: 0,
4897
+ output: Output.object({
4898
+ schema: z5.object({
4899
+ queryType: z5.enum(["aggregate", "list", "targeted", "exploratory"]).describe(
4900
+ "aggregate: ONLY use when the user explicitly asks to COUNT how many documents/items/tickets exist in the knowledge base (e.g. 'how many documents about X?', 'total number of tickets'). NEVER use for: real-world statistics stored in a document, intent statements, how-to questions, error/fault descriptions, configuration questions, or any query that does not explicitly ask for a count of knowledge base entries. When in doubt, choose targeted. list: user wants to enumerate matching items/documents (show me all, list documents about). targeted: use for almost everything \u2014 specific fact, answer, configuration, how-to, error/fault, feature/behavior question. Also use for intent statements and short commands describing a desired state (phrases that state what the user wants to do or achieve, even without an explicit question word). Real-world statistics stored in documents also go here. When in doubt, choose targeted over aggregate or exploratory. exploratory: only for broad conceptual questions needing multi-source synthesis (what is the process for Z, explain how X works, general overview of topic Y)."
4901
+ ),
4902
+ language: z5.string().describe("ISO 639-3 language code of the query (e.g. eng, deu, fra)"),
4903
+ suggestedContextIds: z5.array(z5.enum(contexts.map((c) => c.id))).describe(
4904
+ "IDs of knowledge bases most likely to contain the answer. Return empty array to search all contexts."
4905
+ )
4906
+ })
4907
+ }),
4908
+ toolChoice: "none",
4909
+ system: `You are a query classifier for a multi-knowledge-base retrieval system.
4910
+ Classify the query and identify which knowledge bases are most relevant.
4911
+
4912
+ Available knowledge bases:
4913
+ ${contextDescriptions}
4914
+
4915
+ Guidelines for queryType:
4916
+ - Use "aggregate" ONLY when the query contains explicit counting language (e.g., "how many", "count", "total number", "wie viele"). Short statements, commands, or phrases without a question word are NEVER aggregate \u2014 classify them as targeted.
4917
+ - When in doubt between aggregate and targeted: always choose targeted.
4918
+
4919
+ Guidelines for suggestedContextIds:
4920
+ - Be conservative: only suggest contexts that are genuinely likely to contain the answer.
4921
+ Aim for 2\u20133 focused suggestions rather than listing everything.
4922
+ - Use each knowledge base's name and description (shown above) to judge relevance.
4923
+ - Return an empty array only if you truly cannot determine which contexts are relevant.`,
4924
+ prompt: `Query: ${query}`
4925
+ });
4926
+ return result2.output;
4927
+ }, 3);
4928
+ return result;
4818
4929
  }
4819
4930
 
4820
4931
  // ee/agentic-retrieval/v3/tools.ts
4821
- import { z as z7 } from "zod";
4932
+ import { z as z6 } from "zod";
4822
4933
  import { tool as tool2 } from "ai";
4823
4934
  function buildContextEnum(contexts) {
4824
- return z7.array(z7.enum(contexts.map((c) => c.id))).describe(
4935
+ return z6.array(z6.enum(contexts.map((c) => c.id))).describe(
4825
4936
  contexts.map(
4826
4937
  (c) => `<knowledge_base id="${c.id}" name="${c.name}">${c.description}</knowledge_base>`
4827
4938
  ).join("\n")
@@ -4842,16 +4953,34 @@ function mapSearchMethod(method) {
4842
4953
  if (method === "keyword") return "tsvector";
4843
4954
  return "cosineDistance";
4844
4955
  }
4956
+ function parseGlobalItemIds(globalIds) {
4957
+ const map = /* @__PURE__ */ new Map();
4958
+ for (const gid of globalIds) {
4959
+ const slashIdx = gid.indexOf("/");
4960
+ if (slashIdx === -1) {
4961
+ if (gid) map.set(gid, null);
4962
+ continue;
4963
+ }
4964
+ const contextId = gid.slice(0, slashIdx);
4965
+ const itemId = gid.slice(slashIdx + 1);
4966
+ if (!contextId || !itemId) continue;
4967
+ if (map.get(contextId) === null) continue;
4968
+ const existing = map.get(contextId) ?? [];
4969
+ existing.push(itemId);
4970
+ map.set(contextId, existing);
4971
+ }
4972
+ return map;
4973
+ }
4845
4974
  function createRetrievalTools(params) {
4846
- const { contexts, user, role, updateVirtualFiles } = params;
4975
+ const { contexts, user, role, updateVirtualFiles, preselectedItemsByContext } = params;
4847
4976
  const ctxEnum = buildContextEnum(contexts);
4848
4977
  const count_items_or_chunks = tool2({
4849
4978
  description: "Count items or chunks WITHOUT loading them into context. Use for 'how many', 'count', or 'total number of' queries.",
4850
- inputSchema: z7.object({
4979
+ inputSchema: z6.object({
4851
4980
  knowledge_base_ids: ctxEnum,
4852
- count_what: z7.enum(["items", "chunks"]).describe("Whether to count items (documents) or chunks (pages/sections)"),
4853
- name_contains: z7.string().optional().describe("Only count items whose name contains this text (case-insensitive)"),
4854
- content_query: z7.string().optional().describe(
4981
+ count_what: z6.enum(["items", "chunks"]).describe("Whether to count items (documents) or chunks (pages/sections)"),
4982
+ name_contains: z6.string().optional().describe("Only count items whose name contains this text (case-insensitive)"),
4983
+ content_query: z6.string().optional().describe(
4855
4984
  "Only count chunks matching this search query (uses hybrid search). Only used when count_what is 'chunks'."
4856
4985
  )
4857
4986
  }),
@@ -4860,6 +4989,10 @@ function createRetrievalTools(params) {
4860
4989
  const ctxList = resolveContexts(knowledge_base_ids, contexts);
4861
4990
  const counts = await Promise.all(
4862
4991
  ctxList.map(async (ctx) => {
4992
+ const contextItemIds = preselectedItemsByContext?.get(ctx.id);
4993
+ if (preselectedItemsByContext && contextItemIds === void 0) {
4994
+ return { context: ctx.id, context_name: ctx.name, count: 0 };
4995
+ }
4863
4996
  let count = 0;
4864
4997
  if (count_what === "items") {
4865
4998
  const tableName = getTableName(ctx.id);
@@ -4867,19 +5000,23 @@ function createRetrievalTools(params) {
4867
5000
  if (name_contains) {
4868
5001
  q = q.whereRaw("LOWER(name) LIKE ?", [`%${name_contains.toLowerCase()}%`]);
4869
5002
  }
5003
+ if (Array.isArray(contextItemIds)) {
5004
+ q = q.whereIn("id", contextItemIds);
5005
+ }
4870
5006
  const tableDefinition = convertContextToTableDefinition(ctx);
4871
5007
  q = applyAccessControl(tableDefinition, q, user, tableName);
4872
5008
  const result = await q.first();
4873
5009
  count = Number(result?.count ?? 0);
4874
5010
  } else {
4875
5011
  const chunksTable = getChunksTableName(ctx.id);
5012
+ const baseItemFilters = Array.isArray(contextItemIds) ? [{ id: { in: contextItemIds } }] : [];
4876
5013
  if (content_query) {
4877
5014
  const searchResults = await ctx.search({
4878
5015
  query: content_query,
4879
5016
  method: "hybridSearch",
4880
5017
  limit: 1e4,
4881
5018
  page: 1,
4882
- itemFilters: [],
5019
+ itemFilters: baseItemFilters,
4883
5020
  chunkFilters: [],
4884
5021
  sort: { field: "updatedAt", direction: "desc" },
4885
5022
  user,
@@ -4887,6 +5024,9 @@ function createRetrievalTools(params) {
4887
5024
  trigger: "tool"
4888
5025
  });
4889
5026
  count = searchResults.chunks.length;
5027
+ } else if (Array.isArray(contextItemIds)) {
5028
+ const result = await db2(chunksTable).count("id as count").whereIn("source", contextItemIds).first();
5029
+ count = Number(result?.count ?? 0);
4890
5030
  } else {
4891
5031
  const result = await db2(chunksTable).count("id as count").first();
4892
5032
  count = Number(result?.count ?? 0);
@@ -4902,11 +5042,13 @@ function createRetrievalTools(params) {
4902
5042
  }
4903
5043
  });
4904
5044
  const search_items_by_name = tool2({
4905
- description: "Search for items by their name or external ID. Use only when the user is asking for documents BY TITLE, not by content topic.",
4906
- inputSchema: z7.object({
5045
+ description: "Search for items by their name or external ID. Use when:\n\u2022 The user asks for a document BY TITLE or NAME\n\u2022 The user asks whether a specific named document EXISTS (e.g. 'do you have the X manual?', 'is there a document for Y?')\n\u2022 Any query that references a specific document, manual, or resource by its name rather than by topic\nDo NOT use for topic-based content queries (e.g. 'what are the parameters for X?', 'how do I configure Y?').",
5046
+ inputSchema: z6.object({
4907
5047
  knowledge_base_ids: ctxEnum,
4908
- item_name: z7.string().describe("The name or partial name to search for"),
4909
- limit: z7.number().default(100).describe(
5048
+ item_name: z6.string().describe(
5049
+ "The name or partial name to search for. Uses substring matching, so shorter and more specific terms work better than full phrases. Extract only the core identifying part \u2014 typically the product model, document title, or unique identifier. Do NOT include surrounding descriptors like type words ('manual', 'guide', 'document') or manufacturer names unless they are likely part of the actual document title."
5050
+ ),
5051
+ limit: z6.number().default(100).describe(
4910
5052
  "Max items per knowledge base (max 400). Applies independently to each knowledge base."
4911
5053
  )
4912
5054
  }),
@@ -4914,9 +5056,12 @@ function createRetrievalTools(params) {
4914
5056
  const { db: db2 } = await postgresClient();
4915
5057
  const ctxList = resolveContexts(knowledge_base_ids, contexts);
4916
5058
  const safeLimit = Math.min(limit ?? 100, 400);
4917
- const itemFilters = item_name ? [{ name: { contains: item_name } }] : [];
4918
5059
  const results = await Promise.all(
4919
5060
  ctxList.map(async (ctx) => {
5061
+ const contextItemIds = preselectedItemsByContext?.get(ctx.id);
5062
+ if (preselectedItemsByContext && contextItemIds === void 0) return [];
5063
+ const itemFilters = item_name ? [{ name: { contains: item_name } }] : [];
5064
+ if (Array.isArray(contextItemIds)) itemFilters.push({ id: { in: contextItemIds } });
4920
5065
  const tableName = getTableName(ctx.id);
4921
5066
  const tableDefinition = convertContextToTableDefinition(ctx);
4922
5067
  let q = db2(`${tableName} as items`).select([
@@ -4952,30 +5097,35 @@ function createRetrievalTools(params) {
4952
5097
  }
4953
5098
  });
4954
5099
  const search_content = tool2({
4955
- description: `Search across document content using hybrid, keyword, or semantic search.
5100
+ description: `Search ONE knowledge base for document content using hybrid, keyword, or semantic search.
5101
+ Always make a separate call for each knowledge base you want to search \u2014 never bundle multiple in one call.
4956
5102
 
4957
5103
  Use includeContent: false when you only need to know WHICH documents match (listing, overview, navigation).
4958
5104
  Use includeContent: true when you need the ACTUAL text to answer a question.
4959
5105
 
4960
5106
  For listing queries: always start with includeContent: false, then use dynamic tools to fetch specific pages.`,
4961
- inputSchema: z7.object({
4962
- query: z7.string().describe("Search query about the content you're looking for"),
4963
- knowledge_base_ids: ctxEnum,
4964
- keywords: z7.array(z7.string()).optional().describe("Keywords extracted from the query"),
4965
- searchMethod: z7.enum(["hybrid", "keyword", "semantic"]).default("hybrid").describe(
5107
+ inputSchema: z6.object({
5108
+ query: z6.string().describe("Search query about the content you're looking for"),
5109
+ knowledge_base_id: z6.enum(contexts.map((c) => c.id)).describe(
5110
+ contexts.map(
5111
+ (c) => `<knowledge_base id="${c.id}" name="${c.name}">${c.description}</knowledge_base>`
5112
+ ).join("\n")
5113
+ ),
5114
+ keywords: z6.array(z6.string()).optional().describe("Keywords extracted from the query"),
5115
+ searchMethod: z6.enum(["hybrid", "keyword", "semantic"]).default("hybrid").describe(
4966
5116
  "hybrid: best default (semantic + keyword). keyword: exact terms, product codes, IDs. semantic: conceptual/synonyms."
4967
5117
  ),
4968
- includeContent: z7.boolean().default(true).describe(
5118
+ includeContent: z6.boolean().default(true).describe(
4969
5119
  "false: returns metadata only (document names, scores) \u2014 use for listing/navigation. true: returns full chunk text \u2014 use when you need content to answer a question."
4970
5120
  ),
4971
- item_ids: z7.array(z7.string()).optional().describe("Filter results to specific item IDs"),
4972
- item_names: z7.array(z7.string()).optional().describe("Filter results to items whose name contains one of these strings"),
4973
- item_external_ids: z7.array(z7.string()).optional().describe("Filter results to specific external IDs"),
4974
- limit: z7.number().default(10).describe("Max chunks with content (max 10). Without content, up to 200 are returned.")
5121
+ item_ids: z6.array(z6.string()).optional().describe("Filter results to specific item IDs"),
5122
+ item_names: z6.array(z6.string()).optional().describe("Filter results to items whose name contains one of these strings"),
5123
+ item_external_ids: z6.array(z6.string()).optional().describe("Filter results to specific external IDs"),
5124
+ limit: z6.number().default(20).describe("Max chunks with content (max 20). Without content, up to 200 are returned.")
4975
5125
  }),
4976
5126
  execute: async ({
4977
5127
  query,
4978
- knowledge_base_ids,
5128
+ knowledge_base_id,
4979
5129
  keywords,
4980
5130
  searchMethod,
4981
5131
  includeContent,
@@ -4984,67 +5134,81 @@ For listing queries: always start with includeContent: false, then use dynamic t
4984
5134
  item_external_ids,
4985
5135
  limit
4986
5136
  }) => {
4987
- const ctxList = resolveContexts(knowledge_base_ids, contexts);
4988
- const effectiveLimit = includeContent ? Math.min(limit ?? 10, 10) : Math.min((limit ?? 10) * 20, 400);
4989
- const results = await Promise.all(
4990
- ctxList.map(async (ctx) => {
4991
- const itemFilters = [];
4992
- if (item_ids) itemFilters.push({ id: { in: item_ids } });
4993
- if (item_names)
4994
- itemFilters.push({ name: { or: item_names.map((n) => ({ contains: n })) } });
4995
- if (item_external_ids) itemFilters.push({ external_id: { in: item_external_ids } });
4996
- const effectiveQuery = query || keywords?.join(" ") || "";
4997
- let method = mapSearchMethod(searchMethod ?? "hybrid");
4998
- if (method === "hybridSearch" || method === "cosineDistance") {
4999
- if (!ctx.embedder) {
5000
- console.error(`[EXULU] context "${ctx.id}" does not have an embedder, falling back to tsvector search`);
5001
- method = "tsvector";
5002
- }
5003
- }
5004
- try {
5005
- const { chunks } = await ctx.search({
5006
- query: effectiveQuery,
5007
- keywords,
5008
- method,
5009
- limit: effectiveLimit,
5010
- page: 1,
5011
- itemFilters,
5012
- chunkFilters: [],
5013
- sort: { field: "updatedAt", direction: "desc" },
5014
- user,
5015
- role,
5016
- trigger: "tool"
5017
- });
5018
- return chunks.map(
5019
- (chunk) => ({
5020
- item_name: chunk.item_name,
5021
- item_id: chunk.item_id,
5022
- context: chunk.context?.id ?? ctx.id,
5023
- chunk_id: chunk.chunk_id,
5024
- chunk_index: chunk.chunk_index,
5025
- chunk_content: includeContent ? chunk.chunk_content : void 0,
5026
- metadata: {
5027
- ...chunk.chunk_metadata,
5028
- cosine_distance: chunk.chunk_cosine_distance,
5029
- fts_rank: chunk.chunk_fts_rank,
5030
- hybrid_score: chunk.chunk_hybrid_score
5031
- }
5032
- })
5033
- );
5034
- } catch (err) {
5035
- console.error(`[EXULU] search_content failed for context "${ctx.id}":`, err);
5036
- return [];
5137
+ const [ctx] = resolveContexts([knowledge_base_id], contexts);
5138
+ const effectiveLimit = includeContent ? Math.min(limit ?? 20, 20) : Math.min((limit ?? 20) * 20, 400);
5139
+ const itemFilters = [];
5140
+ if (preselectedItemsByContext) {
5141
+ const contextItemIds = preselectedItemsByContext.get(knowledge_base_id);
5142
+ if (contextItemIds === void 0) {
5143
+ return JSON.stringify([]);
5144
+ }
5145
+ if (Array.isArray(contextItemIds)) {
5146
+ const intersection = item_ids?.length ? item_ids.filter((id) => contextItemIds.includes(id)) : contextItemIds;
5147
+ if (!intersection.length) {
5148
+ return JSON.stringify([]);
5037
5149
  }
5038
- })
5039
- );
5040
- return JSON.stringify(results.flat());
5150
+ itemFilters.push({ id: { in: intersection } });
5151
+ } else if (item_ids?.length) {
5152
+ itemFilters.push({ id: { in: item_ids } });
5153
+ }
5154
+ } else if (item_ids?.length) {
5155
+ itemFilters.push({ id: { in: item_ids } });
5156
+ }
5157
+ if (item_names)
5158
+ itemFilters.push({ name: { or: item_names.map((n) => ({ contains: n })) } });
5159
+ if (item_external_ids) itemFilters.push({ external_id: { in: item_external_ids } });
5160
+ const effectiveQuery = query || keywords?.join(" ") || "";
5161
+ let method = mapSearchMethod(searchMethod ?? "hybrid");
5162
+ if (method === "hybridSearch" || method === "cosineDistance") {
5163
+ if (!ctx.embedder) {
5164
+ console.error(`[EXULU] context "${ctx.id}" does not have an embedder, falling back to tsvector search`);
5165
+ method = "tsvector";
5166
+ }
5167
+ }
5168
+ try {
5169
+ const { chunks } = await ctx.search({
5170
+ query: effectiveQuery,
5171
+ keywords,
5172
+ method,
5173
+ limit: effectiveLimit,
5174
+ page: 1,
5175
+ itemFilters,
5176
+ chunkFilters: [],
5177
+ sort: { field: "updatedAt", direction: "desc" },
5178
+ user,
5179
+ role,
5180
+ trigger: "tool"
5181
+ });
5182
+ return JSON.stringify(
5183
+ chunks.map(
5184
+ (chunk) => ({
5185
+ item_name: chunk.item_name,
5186
+ item_id: chunk.item_id,
5187
+ context: chunk.context?.id ?? ctx.id,
5188
+ chunk_id: chunk.chunk_id,
5189
+ chunk_index: chunk.chunk_index,
5190
+ chunk_content: includeContent ? chunk.chunk_content : void 0,
5191
+ metadata: {
5192
+ ...chunk.chunk_metadata,
5193
+ cosine_distance: chunk.chunk_cosine_distance,
5194
+ fts_rank: chunk.chunk_fts_rank,
5195
+ hybrid_score: chunk.chunk_hybrid_score
5196
+ }
5197
+ })
5198
+ )
5199
+ );
5200
+ } catch (err) {
5201
+ console.error(`[EXULU] search_content failed for context "${ctx.id}":`, err);
5202
+ return JSON.stringify([]);
5203
+ }
5041
5204
  }
5042
5205
  });
5043
5206
  const save_search_results = tool2({
5044
- description: `Execute a search and save ALL results to the virtual filesystem WITHOUT loading them into context.
5207
+ description: `Execute a search on ONE knowledge base and save ALL results to the virtual filesystem WITHOUT loading them into context.
5208
+ Always make a separate call for each knowledge base you want to search.
5045
5209
 
5046
5210
  Use this when you expect many results (>20) and need to filter iteratively:
5047
- 1. Call save_search_results to save up to 1000 results to /search_results.txt
5211
+ 1. Call save_search_results (once per knowledge base) to save up to 1000 results to /search_results_{knowledge_base_id}.txt
5048
5212
  2. Use bash grep/awk to identify relevant chunks by pattern
5049
5213
  3. Use dynamic get_content tools to load only the specific chunks you need
5050
5214
 
@@ -5059,40 +5223,49 @@ SCORE: ...
5059
5223
  ---CONTENT START---
5060
5224
  (content or placeholder)
5061
5225
  ---CONTENT END---`,
5062
- inputSchema: z7.object({
5063
- knowledge_base_ids: ctxEnum,
5064
- query: z7.string().describe("Search query"),
5065
- searchMethod: z7.enum(["hybrid", "keyword", "semantic"]).default("hybrid"),
5066
- limit: z7.number().max(1e3).default(100).describe("Max results to save (max 1000)"),
5067
- includeContent: z7.boolean().default(true).describe(
5226
+ inputSchema: z6.object({
5227
+ knowledge_base_id: z6.enum(contexts.map((c) => c.id)).describe(
5228
+ contexts.map(
5229
+ (c) => `<knowledge_base id="${c.id}" name="${c.name}">${c.description}</knowledge_base>`
5230
+ ).join("\n")
5231
+ ),
5232
+ query: z6.string().describe("Search query"),
5233
+ searchMethod: z6.enum(["hybrid", "keyword", "semantic"]).default("hybrid"),
5234
+ limit: z6.number().max(1e3).default(100).describe("Max results to save (max 1000)"),
5235
+ includeContent: z6.boolean().default(true).describe(
5068
5236
  "Whether to include chunk text in the saved file. False saves tokens \u2014 use true only if you need to grep content."
5069
5237
  )
5070
5238
  }),
5071
- execute: async ({ query, knowledge_base_ids, searchMethod, limit, includeContent }) => {
5072
- const ctxList = resolveContexts(knowledge_base_ids, contexts);
5073
- const results = await Promise.all(
5074
- ctxList.map(async (ctx) => {
5075
- try {
5076
- const { chunks: chunks2 } = await ctx.search({
5077
- query,
5078
- method: mapSearchMethod(searchMethod ?? "hybrid"),
5079
- limit: Math.min(limit ?? 100, 1e3),
5080
- page: 1,
5081
- itemFilters: [],
5082
- chunkFilters: [],
5083
- sort: { field: "updatedAt", direction: "desc" },
5084
- user,
5085
- role,
5086
- trigger: "tool"
5087
- });
5088
- return chunks2;
5089
- } catch (err) {
5090
- console.error(`[EXULU] save_search_results failed for context "${ctx.id}":`, err);
5091
- return [];
5092
- }
5093
- })
5094
- );
5095
- const chunks = results.flat();
5239
+ execute: async ({ query, knowledge_base_id, searchMethod, limit, includeContent }) => {
5240
+ const [ctx] = resolveContexts([knowledge_base_id], contexts);
5241
+ const contextItemIds = preselectedItemsByContext?.get(knowledge_base_id);
5242
+ if (preselectedItemsByContext && contextItemIds === void 0) {
5243
+ return JSON.stringify({
5244
+ success: true,
5245
+ results_count: 0,
5246
+ message: `Context "${knowledge_base_id}" not in preselected scope \u2014 skipped.`
5247
+ });
5248
+ }
5249
+ const itemFilters = Array.isArray(contextItemIds) ? [{ id: { in: contextItemIds } }] : [];
5250
+ let chunks = [];
5251
+ try {
5252
+ const result = await ctx.search({
5253
+ query,
5254
+ method: mapSearchMethod(searchMethod ?? "hybrid"),
5255
+ limit: Math.min(limit ?? 100, 1e3),
5256
+ page: 1,
5257
+ itemFilters,
5258
+ chunkFilters: [],
5259
+ sort: { field: "updatedAt", direction: "desc" },
5260
+ user,
5261
+ role,
5262
+ trigger: "tool"
5263
+ });
5264
+ chunks = result.chunks;
5265
+ } catch (err) {
5266
+ console.error(`[EXULU] save_search_results failed for context "${ctx.id}":`, err);
5267
+ }
5268
+ const fileName = `search_results_${ctx.id}.txt`;
5096
5269
  const fileContent = chunks.map(
5097
5270
  (chunk, i) => `### RESULT ${i + 1} ###
5098
5271
  ITEM_NAME: ${chunk.item_name}
@@ -5107,14 +5280,14 @@ ${includeContent && chunk.chunk_content ? chunk.chunk_content : "[use includeCon
5107
5280
  `
5108
5281
  ).join("\n");
5109
5282
  await updateVirtualFiles([
5110
- { path: "search_results.txt", content: fileContent },
5283
+ { path: fileName, content: fileContent },
5111
5284
  {
5112
- path: "search_metadata.json",
5285
+ path: `search_metadata_${ctx.id}.json`,
5113
5286
  content: JSON.stringify({
5114
5287
  query,
5115
5288
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5116
5289
  results_count: chunks.length,
5117
- contexts: ctxList.map((c) => c.id),
5290
+ context: ctx.id,
5118
5291
  method: searchMethod
5119
5292
  })
5120
5293
  }
@@ -5122,11 +5295,11 @@ ${includeContent && chunk.chunk_content ? chunk.chunk_content : "[use includeCon
5122
5295
  return JSON.stringify({
5123
5296
  success: true,
5124
5297
  results_count: chunks.length,
5125
- message: `Saved ${chunks.length} results to /search_results.txt`,
5298
+ message: `Saved ${chunks.length} results to /${fileName}`,
5126
5299
  grep_examples: [
5127
- "grep -i 'keyword' search_results.txt | head -20",
5128
- "grep 'ITEM_NAME:' search_results.txt",
5129
- "grep -B 5 'pattern' search_results.txt | grep 'CHUNK_ID:'"
5300
+ `grep -i 'keyword' ${fileName} | head -20`,
5301
+ `grep 'ITEM_NAME:' ${fileName}`,
5302
+ `grep -B 5 'pattern' ${fileName} | grep 'CHUNK_ID:'`
5130
5303
  ]
5131
5304
  });
5132
5305
  }
@@ -5148,9 +5321,10 @@ another component will do that based on what you retrieve.
5148
5321
  Always respond in the SAME LANGUAGE as the user's query.
5149
5322
  Always write search queries in the SAME LANGUAGE as the user's query \u2014 do NOT translate to English.
5150
5323
 
5151
- SEARCH APPROACH \u2014 go wide first, then deep:
5152
- 1. First step: search broadly across all sources the system instructions indicate \u2014 do NOT
5153
- pre-filter to a single context on step 1.
5324
+ SEARCH APPROACH \u2014 one knowledge base at a time, then go deep:
5325
+ 1. search_content and save_search_results accept ONE knowledge base per call. Make a separate
5326
+ call for each knowledge base you need to cover \u2014 never skip one. Search all relevant
5327
+ knowledge bases before concluding, even if the first one already returned good results.
5154
5328
  2. After finding a relevant document, use get_more_content_from_{item} dynamic tools to load
5155
5329
  additional pages/sections. The specific answer is often NOT in the first retrieved chunk \u2014
5156
5330
  always explore adjacent content before concluding.
@@ -5166,9 +5340,8 @@ var AGGREGATE_INSTRUCTIONS = `
5166
5340
  ${BASE_INSTRUCTIONS}
5167
5341
 
5168
5342
  STRATEGY: This is a COUNTING or AGGREGATION query.
5169
- - Use count_items_or_chunks exclusively
5343
+ - Use count_items_or_chunks exclusively \u2014 it accepts multiple knowledge bases in one call for efficiency
5170
5344
  - Do NOT use search_content \u2014 it loads unnecessary data
5171
- - Search ALL contexts in parallel in a single tool call
5172
5345
  - Return immediately after counting \u2014 one step is sufficient
5173
5346
  - If the count needs a content filter, use content_query parameter
5174
5347
  `.trim();
@@ -5201,9 +5374,23 @@ Search language:
5201
5374
  - Always write search queries in the SAME LANGUAGE as the user's query.
5202
5375
  - Do NOT translate the query to English \u2014 the documents are indexed in their original language.
5203
5376
 
5204
- Step 1 \u2014 wide hybrid search (includeContent: true, limit 10):
5205
- - Search broadly across all sources per the system instructions \u2014 do not limit to 1 context.
5206
- - This gives you the best results from every relevant source at once.
5377
+ Step 1 \u2014 match the opening move to what the query actually needs:
5378
+
5379
+ Query references a SPECIFIC NAMED DOCUMENT (product manual, titled report, named file):
5380
+ \u2192 ALWAYS start with search_items_by_name \u2014 searches document name/title directly
5381
+ \u2192 Only proceed to load content if the document is found
5382
+
5383
+ Query asks WHETHER a topic EXISTS or WHAT documents cover a topic (no specific title given):
5384
+ \u2192 search_content with includeContent: false
5385
+ \u2192 Returns matching document names without loading chunk text \u2014 efficient and precise
5386
+ \u2192 Load content with dynamic get_{item}_page_{n}_content tools only if needed in step 2
5387
+
5388
+ Query asks for CONTENT itself (procedures, parameters, explanations, how-to):
5389
+ \u2192 search_content with includeContent: true, limit 20, searchMethod: "hybrid"
5390
+ \u2192 Make one call per knowledge base \u2014 search each separately before concluding
5391
+
5392
+ Query provides an EXACT TERM (error code, product code, ID, parameter name):
5393
+ \u2192 search_content with searchMethod: "keyword"
5207
5394
 
5208
5395
  Step 2+ \u2014 depth and follow-up:
5209
5396
  - For any relevant document found with fewer than 5 chunks, use get_more_content_from_{item}
@@ -5213,19 +5400,9 @@ Step 2+ \u2014 depth and follow-up:
5213
5400
  - Try alternative phrasings if the first query doesn't surface the right answer.
5214
5401
 
5215
5402
  Product-specific filtering:
5216
- - When the query mentions a specific product (e.g., "FST-3", "ECO"), you MAY use
5217
- item_names: ["<product>"] on a follow-up search to narrow results \u2014 but only after an initial
5403
+ - When the query mentions a specific named entity (product, model, version), you MAY use
5404
+ item_names: ["<entity>"] on a follow-up search to narrow results \u2014 but only after an initial
5218
5405
  wide search. Never start with item_names filtering alone.
5219
-
5220
- Two-step approach \u2014 use includeContent: false first:
5221
- - Only when you expect many results (>20) and need to identify the right document first.
5222
- - Step 1: search_content with includeContent: false \u2192 see which documents/chunks match.
5223
- - Step 2: use dynamic get_{item}_page_{n}_content tools to load specific pages.
5224
-
5225
- Search method selection:
5226
- - hybrid (default): best for most queries
5227
- - keyword: exact product codes, document IDs, error codes
5228
- - semantic: conceptual questions, synonyms, paraphrasing
5229
5406
  `.trim();
5230
5407
  var EXPLORATORY_INSTRUCTIONS = `
5231
5408
  ${BASE_INSTRUCTIONS}
@@ -5233,13 +5410,13 @@ ${BASE_INSTRUCTIONS}
5233
5410
  STRATEGY: This is an EXPLORATORY query \u2014 general question requiring broad search.
5234
5411
 
5235
5412
  Recommended approach:
5236
- 1. Start with a wide hybrid search across all relevant contexts (includeContent: true, limit: 10)
5413
+ 1. Search each relevant knowledge base separately with hybrid search (includeContent: true, limit: 20) \u2014 one call per knowledge base
5237
5414
  2. If results are insufficient: try alternative search terms or different search method
5238
5415
  3. Use save_search_results + bash grep when you need to scan many results without context bloat
5239
5416
  4. Use dynamic get_more_content_from_{item} tools to read adjacent pages when a relevant item is found
5240
5417
 
5241
5418
  When to declare done:
5242
- - You have retrieved chunks that cover the key aspects of the query
5419
+ - You have retrieved chunks that cover the key aspects of the query from all relevant knowledge bases
5243
5420
  - OR you have tried 3+ different search strategies and found nothing relevant
5244
5421
 
5245
5422
  Do NOT use count_items_or_chunks for exploratory queries \u2014 the user wants content, not statistics.
@@ -5254,7 +5431,7 @@ var STRATEGIES = {
5254
5431
  },
5255
5432
  list: {
5256
5433
  queryType: "list",
5257
- stepBudget: 2,
5434
+ stepBudget: 3,
5258
5435
  retrieval_tools: ["count_items_or_chunks", "search_items_by_name", "search_content"],
5259
5436
  include_bash: false,
5260
5437
  instructions: LIST_INSTRUCTIONS
@@ -5268,7 +5445,7 @@ var STRATEGIES = {
5268
5445
  },
5269
5446
  exploratory: {
5270
5447
  queryType: "exploratory",
5271
- stepBudget: 4,
5448
+ stepBudget: 5,
5272
5449
  retrieval_tools: [
5273
5450
  "count_items_or_chunks",
5274
5451
  "search_items_by_name",
@@ -5282,28 +5459,10 @@ var STRATEGIES = {
5282
5459
 
5283
5460
  // ee/agentic-retrieval/v3/agent-loop.ts
5284
5461
  import { generateText as generateText2, stepCountIs, tool as tool4 } from "ai";
5285
- import { z as z9 } from "zod";
5286
-
5287
- // src/utils/with-retry.ts
5288
- async function withRetry(generateFn, maxRetries = 3) {
5289
- let lastError;
5290
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
5291
- try {
5292
- return await generateFn();
5293
- } catch (error) {
5294
- lastError = error;
5295
- console.error(`[EXULU] generateText attempt ${attempt} failed:`, error);
5296
- if (attempt === maxRetries) {
5297
- throw error;
5298
- }
5299
- await new Promise((resolve4) => setTimeout(resolve4, Math.pow(2, attempt) * 1e3));
5300
- }
5301
- }
5302
- throw lastError;
5303
- }
5462
+ import { z as z8 } from "zod";
5304
5463
 
5305
5464
  // ee/agentic-retrieval/v3/dynamic-tools.ts
5306
- import { z as z8 } from "zod";
5465
+ import { z as z7 } from "zod";
5307
5466
  import { tool as tool3 } from "ai";
5308
5467
  async function createDynamicTools(chunks, hadExcludedContent) {
5309
5468
  const { db: db2 } = await postgresClient();
@@ -5322,9 +5481,9 @@ async function createDynamicTools(chunks, hadExcludedContent) {
5322
5481
  const capturedChunk = chunk;
5323
5482
  tools[browseToolName] = tool3({
5324
5483
  description: `"${chunk.item_name}" has ${total} pages/chunks. Use this to read a range of pages from it.`,
5325
- inputSchema: z8.object({
5326
- from_index: z8.number().min(1).default(1).describe("Starting chunk index (1-based)"),
5327
- to_index: z8.number().max(total).describe(`Ending chunk index (max ${total})`)
5484
+ inputSchema: z7.object({
5485
+ from_index: z7.number().min(1).default(1).describe("Starting chunk index (1-based)"),
5486
+ to_index: z7.number().max(total).describe(`Ending chunk index (max ${total})`)
5328
5487
  }),
5329
5488
  execute: async ({ from_index, to_index }) => {
5330
5489
  const { db: db22 } = await postgresClient();
@@ -5353,8 +5512,8 @@ async function createDynamicTools(chunks, hadExcludedContent) {
5353
5512
  const capturedChunk = chunk;
5354
5513
  tools[pageToolName] = tool3({
5355
5514
  description: `Load the full text of page ${chunk.chunk_index} from "${chunk.item_name}"`,
5356
- inputSchema: z8.object({
5357
- reasoning: z8.string().describe("Why you need this specific page's content")
5515
+ inputSchema: z7.object({
5516
+ reasoning: z7.string().describe("Why you need this specific page's content")
5358
5517
  }),
5359
5518
  execute: async () => {
5360
5519
  const { db: db22 } = await postgresClient();
@@ -5381,8 +5540,8 @@ async function createDynamicTools(chunks, hadExcludedContent) {
5381
5540
  var FINISH_TOOL_NAME = "finish_retrieval";
5382
5541
  var finishRetrievalTool = tool4({
5383
5542
  description: "Call this tool when you have retrieved sufficient information and no further searches are needed. You MUST call this tool to signal that retrieval is complete \u2014 do not write a text conclusion.",
5384
- inputSchema: z9.object({
5385
- reasoning: z9.string().describe("One sentence explaining why retrieval is complete")
5543
+ inputSchema: z8.object({
5544
+ reasoning: z8.string().describe("One sentence explaining why retrieval is complete")
5386
5545
  }),
5387
5546
  execute: async ({ reasoning }) => JSON.stringify({ finished: true, reasoning })
5388
5547
  });
@@ -5415,7 +5574,7 @@ function extractChunksFromToolResults(toolResults) {
5415
5574
  return chunks;
5416
5575
  }
5417
5576
  async function* runAgentLoop(params) {
5418
- const { query, strategy, tools, model, reranker, contextGuidance, customInstructions, onStepComplete } = params;
5577
+ const { query, strategy, tools, model, reranker, contextGuidance, customInstructions, sessionId, onStepComplete, onTrajectoryStep } = params;
5419
5578
  const output = {
5420
5579
  steps: [],
5421
5580
  reasoning: [],
@@ -5471,6 +5630,13 @@ ${customInstructions}` : ""
5471
5630
  }
5472
5631
  messages.push(...result.response.messages);
5473
5632
  let stepChunks = extractChunksFromToolResults(result.toolResults);
5633
+ const seenChunkIds = /* @__PURE__ */ new Set();
5634
+ stepChunks = stepChunks.filter((c) => {
5635
+ if (!c.chunk_id) return true;
5636
+ if (seenChunkIds.has(c.chunk_id)) return false;
5637
+ seenChunkIds.add(c.chunk_id);
5638
+ return true;
5639
+ });
5474
5640
  const hadExcludedContent = result.toolCalls?.some(
5475
5641
  (tc) => tc.toolName === "search_content" && tc.input?.includeContent === false || tc.toolName === "search_items_by_name"
5476
5642
  );
@@ -5480,9 +5646,15 @@ ${customInstructions}` : ""
5480
5646
  }
5481
5647
  const newDynamic = await createDynamicTools(stepChunks, hadExcludedContent);
5482
5648
  Object.assign(dynamicTools, newDynamic);
5649
+ if (sessionId && Object.keys(newDynamic).length > 0) {
5650
+ registerSessionTools(sessionId, newDynamic);
5651
+ }
5483
5652
  forceDepthExploration = stepChunks.length > 0 && stepChunks.length < 5 && Object.keys(newDynamic).length > 0 && step < strategy.stepBudget - 2;
5484
5653
  for (const tc of result.toolCalls ?? []) {
5485
5654
  if (SEARCH_TOOL_NAMES.has(tc.toolName)) {
5655
+ if (tc.input?.knowledge_base_id) {
5656
+ searchedContextIds.add(tc.input.knowledge_base_id);
5657
+ }
5486
5658
  for (const id of tc.input?.knowledge_base_ids ?? []) {
5487
5659
  searchedContextIds.add(id);
5488
5660
  }
@@ -5517,9 +5689,30 @@ ${customInstructions}` : ""
5517
5689
  output: stepChunks
5518
5690
  })) ?? []
5519
5691
  });
5520
- output.chunks.push(...stepChunks);
5692
+ const existingChunkIds = new Set(output.chunks.map((c) => c.chunk_id).filter(Boolean));
5693
+ output.chunks.push(...stepChunks.filter((c) => !c.chunk_id || !existingChunkIds.has(c.chunk_id)));
5521
5694
  output.usage.push(result.usage);
5522
5695
  onStepComplete?.(stepRecord);
5696
+ if (onTrajectoryStep) {
5697
+ const toolResultMap = /* @__PURE__ */ new Map();
5698
+ for (const tr of result.toolResults ?? []) {
5699
+ toolResultMap.set(tr.toolCallId, tr.output ?? tr.result);
5700
+ }
5701
+ onTrajectoryStep({
5702
+ stepNumber: step + 1,
5703
+ systemPrompt: stepSystemPrompt,
5704
+ text: result.text ?? "",
5705
+ toolCalls: result.toolCalls?.map((tc) => ({
5706
+ name: tc.toolName,
5707
+ id: tc.toolCallId,
5708
+ input: tc.input,
5709
+ output: toolResultMap.get(tc.toolCallId)
5710
+ })) ?? [],
5711
+ chunks: stepChunks,
5712
+ dynamicToolsCreated: Object.keys(newDynamic),
5713
+ tokens: result.usage?.totalTokens ?? 0
5714
+ });
5715
+ }
5523
5716
  yield { ...output };
5524
5717
  const calledFinish = result.toolCalls?.some(
5525
5718
  (tc) => tc.toolName === FINISH_TOOL_NAME
@@ -5537,21 +5730,23 @@ ${customInstructions}` : ""
5537
5730
  }
5538
5731
 
5539
5732
  // ee/agentic-retrieval/v3/trajectory.ts
5540
- import * as fs2 from "fs/promises";
5733
+ import * as fs from "fs/promises";
5541
5734
  import * as path from "path";
5542
5735
  var trajectoryRegistry = {
5543
5736
  lastFile: void 0
5544
5737
  };
5545
5738
  var TrajectoryLogger = class {
5546
5739
  data;
5740
+ richSteps = [];
5547
5741
  startTime = Date.now();
5548
5742
  logDir;
5549
- constructor(query, classification, logDir = path.join(process.cwd(), "ee/agentic-retrieval/logs")) {
5743
+ constructor(query, classification, logDir = path.join(process.cwd(), "ee/agentic-retrieval/logs"), preselectedItemIds) {
5550
5744
  this.logDir = logDir;
5551
5745
  this.data = {
5552
5746
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5553
5747
  query,
5554
5748
  classification,
5749
+ preselectedItemIds: preselectedItemIds?.length ? preselectedItemIds : void 0,
5555
5750
  steps: [],
5556
5751
  final: {
5557
5752
  total_chunks: 0,
@@ -5572,23 +5767,181 @@ var TrajectoryLogger = class {
5572
5767
  tokens: step.tokens
5573
5768
  });
5574
5769
  }
5575
- async finalize(output, success, error) {
5770
+ recordRichStep(data) {
5771
+ this.richSteps.push(data);
5772
+ }
5773
+ toMarkdown(durationMs, success, error) {
5774
+ const totalTokens = this.richSteps.reduce((sum, s) => sum + s.tokens, 0);
5775
+ const totalChunks = this.richSteps.reduce((sum, s) => sum + s.chunks.length, 0);
5776
+ const status = success ? "\u2713 Success" : `\u2717 Failed${error ? `: ${error.message}` : ""}`;
5777
+ const lines = [];
5778
+ lines.push(`# Agentic Retrieval \u2014 ${this.data.timestamp}`);
5779
+ lines.push("");
5780
+ lines.push(`**Query:** ${this.data.query} `);
5781
+ lines.push(
5782
+ `**Duration:** ${(durationMs / 1e3).toFixed(1)}s | **Tokens:** ${totalTokens} | **Status:** ${status}`
5783
+ );
5784
+ lines.push("");
5785
+ lines.push("## Classification");
5786
+ lines.push("");
5787
+ lines.push(`- **Type:** \`${this.data.classification.queryType}\``);
5788
+ lines.push(`- **Language:** \`${this.data.classification.language}\``);
5789
+ const suggested = this.data.classification.suggestedContextIds;
5790
+ lines.push(
5791
+ `- **Suggested contexts:** ${suggested.length > 0 ? suggested.map((id) => `\`${id}\``).join(", ") : "*(all)*"}`
5792
+ );
5793
+ if (this.data.preselectedItemIds?.length) {
5794
+ lines.push(
5795
+ `- **Preselected item IDs:** ${this.data.preselectedItemIds.map((id) => `\`${id}\``).join(", ")}`
5796
+ );
5797
+ }
5798
+ lines.push("");
5799
+ lines.push("---");
5800
+ lines.push("");
5801
+ const firstStep = this.richSteps[0];
5802
+ if (firstStep) {
5803
+ lines.push("## System Prompt");
5804
+ lines.push("");
5805
+ lines.push("<details>");
5806
+ lines.push("<summary>View system prompt</summary>");
5807
+ lines.push("");
5808
+ lines.push("```");
5809
+ lines.push(firstStep.systemPrompt);
5810
+ lines.push("```");
5811
+ lines.push("");
5812
+ lines.push("</details>");
5813
+ lines.push("");
5814
+ lines.push("---");
5815
+ lines.push("");
5816
+ }
5817
+ for (const step of this.richSteps) {
5818
+ const toolLabel = step.toolCalls.map((tc) => `\`${tc.name}\``).join(", ") || "*(no tool calls)*";
5819
+ lines.push(`## Step ${step.stepNumber} \u2014 ${toolLabel}`);
5820
+ lines.push("");
5821
+ const dynLabel = step.dynamicToolsCreated.length > 0 ? step.dynamicToolsCreated.map((t) => `\`${t}\``).join(", ") : "none";
5822
+ lines.push(
5823
+ `**Tokens:** ${step.tokens} | **Chunks retrieved:** ${step.chunks.length} | **Dynamic tools created:** ${dynLabel}`
5824
+ );
5825
+ lines.push("");
5826
+ if (step.text) {
5827
+ lines.push("### Reasoning");
5828
+ lines.push("");
5829
+ lines.push(step.text);
5830
+ lines.push("");
5831
+ }
5832
+ if (step.toolCalls.length > 0) {
5833
+ lines.push("### Tool Calls");
5834
+ lines.push("");
5835
+ for (const [i, tc] of step.toolCalls.entries()) {
5836
+ lines.push(`#### ${i + 1}. \`${tc.name}\``);
5837
+ lines.push("");
5838
+ lines.push("**Input:**");
5839
+ lines.push("```json");
5840
+ lines.push(JSON.stringify(tc.input, null, 2));
5841
+ lines.push("```");
5842
+ lines.push("");
5843
+ if (tc.output !== void 0) {
5844
+ let parsedOutput;
5845
+ try {
5846
+ parsedOutput = typeof tc.output === "string" ? JSON.parse(tc.output) : tc.output;
5847
+ } catch {
5848
+ parsedOutput = tc.output;
5849
+ }
5850
+ const outputStr = JSON.stringify(parsedOutput, null, 2);
5851
+ const truncated = outputStr.length > 2e3;
5852
+ lines.push("**Output:**");
5853
+ lines.push("```json");
5854
+ lines.push(truncated ? `${outputStr.slice(0, 2e3)}
5855
+ \u2026 (truncated)` : outputStr);
5856
+ lines.push("```");
5857
+ lines.push("");
5858
+ }
5859
+ }
5860
+ }
5861
+ if (step.chunks.length > 0) {
5862
+ lines.push("### Chunks Retrieved");
5863
+ lines.push("");
5864
+ lines.push("| # | Item | Context | Chunk | Score |");
5865
+ lines.push("|---|------|---------|-------|-------|");
5866
+ for (const [i, c] of step.chunks.entries()) {
5867
+ const score = c.metadata?.hybrid_score ?? c.metadata?.cosine_distance ?? c.metadata?.fts_rank ?? "\u2014";
5868
+ const scoreStr = typeof score === "number" ? score.toFixed(4) : String(score);
5869
+ lines.push(
5870
+ `| ${i + 1} | ${c.item_name ?? "\u2014"} | \`${c.context}\` | ${c.chunk_index ?? "\u2014"} | ${scoreStr} |`
5871
+ );
5872
+ }
5873
+ lines.push("");
5874
+ const withContent = step.chunks.filter((c) => c.chunk_content);
5875
+ if (withContent.length > 0) {
5876
+ lines.push("<details>");
5877
+ lines.push("<summary>View chunk content</summary>");
5878
+ lines.push("");
5879
+ for (const c of withContent) {
5880
+ lines.push(`**${c.item_name} (chunk ${c.chunk_index}):**`);
5881
+ lines.push("");
5882
+ const content = (c.chunk_content ?? "").trim();
5883
+ lines.push(`> ${content.split("\n").join("\n> ")}`);
5884
+ lines.push("");
5885
+ }
5886
+ lines.push("</details>");
5887
+ lines.push("");
5888
+ }
5889
+ }
5890
+ if (firstStep && step.stepNumber > 1 && step.systemPrompt !== firstStep.systemPrompt) {
5891
+ const addendum = step.systemPrompt.slice(firstStep.systemPrompt.length).trim();
5892
+ if (addendum) {
5893
+ lines.push("<details>");
5894
+ lines.push("<summary>System prompt addendum (this step only)</summary>");
5895
+ lines.push("");
5896
+ lines.push("```");
5897
+ lines.push(addendum);
5898
+ lines.push("```");
5899
+ lines.push("");
5900
+ lines.push("</details>");
5901
+ lines.push("");
5902
+ }
5903
+ }
5904
+ lines.push("---");
5905
+ lines.push("");
5906
+ }
5907
+ lines.push("## Summary");
5908
+ lines.push("");
5909
+ lines.push("| Metric | Value |");
5910
+ lines.push("|--------|-------|");
5911
+ lines.push(`| Steps | ${this.richSteps.length} |`);
5912
+ lines.push(`| Total chunks | ${totalChunks} |`);
5913
+ lines.push(`| Total tokens | ${totalTokens} |`);
5914
+ lines.push(`| Duration | ${(durationMs / 1e3).toFixed(1)}s |`);
5915
+ lines.push(`| Status | ${status} |`);
5916
+ if (error) {
5917
+ lines.push(`| Error | ${error.message} |`);
5918
+ }
5919
+ lines.push("");
5920
+ return lines.join("\n");
5921
+ }
5922
+ async finalize(output, success, error, writeFiles = false) {
5923
+ const durationMs = Date.now() - this.startTime;
5576
5924
  this.data.final = {
5577
5925
  total_chunks: output.chunks.length,
5578
5926
  total_steps: output.steps.length,
5579
5927
  total_tokens: output.totalTokens,
5580
- duration_ms: Date.now() - this.startTime,
5928
+ duration_ms: durationMs,
5581
5929
  success,
5582
5930
  error: error?.message
5583
5931
  };
5932
+ if (!writeFiles) return void 0;
5584
5933
  try {
5585
- await fs2.mkdir(this.logDir, { recursive: true });
5586
- const filename = `trajectory_${Date.now()}.json`;
5587
- const fullPath = path.join(this.logDir, filename);
5588
- await fs2.writeFile(fullPath, JSON.stringify(this.data, null, 2), "utf-8");
5589
- console.log(`[EXULU] v3 trajectory saved: ${filename}`);
5590
- trajectoryRegistry.lastFile = fullPath;
5591
- return fullPath;
5934
+ await fs.mkdir(this.logDir, { recursive: true });
5935
+ const ts = Date.now();
5936
+ const jsonPath = path.join(this.logDir, `trajectory_${ts}.json`);
5937
+ const mdPath = path.join(this.logDir, `trajectory_${ts}.md`);
5938
+ await Promise.all([
5939
+ fs.writeFile(jsonPath, JSON.stringify(this.data, null, 2), "utf-8"),
5940
+ fs.writeFile(mdPath, this.toMarkdown(durationMs, success, error), "utf-8")
5941
+ ]);
5942
+ console.log(`[EXULU] v3 trajectory saved: trajectory_${ts}.json + trajectory_${ts}.md`);
5943
+ trajectoryRegistry.lastFile = jsonPath;
5944
+ return jsonPath;
5592
5945
  } catch (e) {
5593
5946
  console.error("[EXULU] v3 failed to write trajectory:", e);
5594
5947
  return void 0;
@@ -5605,14 +5958,19 @@ async function* executeV3({
5605
5958
  model,
5606
5959
  user,
5607
5960
  role,
5608
- customInstructions
5961
+ customInstructions,
5962
+ logTrajectory,
5963
+ sessionId,
5964
+ preselectedItemIds
5609
5965
  }) {
5966
+ const preselectedByContext = preselectedItemIds?.length ? parseGlobalItemIds(preselectedItemIds) : void 0;
5967
+ const activeContexts = preselectedByContext?.size ? contexts.filter((c) => preselectedByContext.has(c.id)) : contexts;
5610
5968
  console.log("[EXULU] v3 \u2014 sampling contexts");
5611
- const samples = await sampler.getSamples(contexts, user, role);
5969
+ const samples = await sampler.getSamples(activeContexts, user, role);
5612
5970
  console.log("[EXULU] v3 \u2014 classifying query");
5613
5971
  let classification;
5614
5972
  try {
5615
- classification = await classifyQuery(query, contexts, samples, model);
5973
+ classification = await classifyQuery(query, activeContexts, samples, model);
5616
5974
  } catch (err) {
5617
5975
  console.warn("[EXULU] v3 \u2014 classification failed, falling back to exploratory:", err);
5618
5976
  classification = {
@@ -5624,15 +5982,18 @@ async function* executeV3({
5624
5982
  console.log("[EXULU] v3 \u2014 classified as:", classification);
5625
5983
  const strategy = STRATEGIES[classification.queryType];
5626
5984
  const suggestedIds = classification.suggestedContextIds;
5627
- const fallbackIds = contexts.filter((c) => !suggestedIds.includes(c.id)).map((c) => c.id);
5628
- const contextGuidance = suggestedIds.length > 0 ? `Suggested priority contexts: [${suggestedIds.join(", ")}]. Also available: [${fallbackIds.join(", ")}]. Custom instructions may require searching additional or all contexts \u2014 follow them.` : `All contexts available: [${contexts.map((c) => c.id).join(", ")}].`;
5985
+ const fallbackIds = activeContexts.filter((c) => !suggestedIds.includes(c.id)).map((c) => c.id);
5986
+ const contextBase = suggestedIds.length > 0 ? `Suggested priority contexts: [${suggestedIds.join(", ")}]. Also available: [${fallbackIds.join(", ")}]. Custom instructions may require searching additional or all contexts \u2014 follow them.` : `All contexts available: [${activeContexts.map((c) => c.id).join(", ")}].`;
5987
+ const preselectedNote = preselectedByContext?.size ? `
5988
+ SCOPE CONSTRAINT: Retrieval is scoped to preselected items/contexts. Per context: ${[...preselectedByContext.entries()].map(([ctx, ids]) => ids === null ? `${ctx} (full context)` : `${ctx} (${ids.length} item${ids.length === 1 ? "" : "s"})`).join(", ")}. All tools enforce this scope automatically. For full-context entries you may search freely; for item-restricted entries do NOT use search_items_by_name for discovery \u2014 go directly to search_content or save_search_results.` : "";
5989
+ const contextGuidance = contextBase + preselectedNote;
5629
5990
  const bashToolkit = await createBashTool({ files: {} });
5630
5991
  const retrievalTools = createRetrievalTools({
5631
- contexts,
5632
- // ALL contexts — agent decides which to search based on context guidance
5992
+ contexts: activeContexts,
5633
5993
  user,
5634
5994
  role,
5635
- updateVirtualFiles: (files) => bashToolkit.sandbox.writeFiles(files)
5995
+ updateVirtualFiles: (files) => bashToolkit.sandbox.writeFiles(files),
5996
+ preselectedItemsByContext: preselectedByContext
5636
5997
  });
5637
5998
  const activeTools = {};
5638
5999
  for (const name of strategy.retrieval_tools) {
@@ -5643,7 +6004,7 @@ async function* executeV3({
5643
6004
  if (strategy.include_bash) {
5644
6005
  Object.assign(activeTools, bashToolkit.tools);
5645
6006
  }
5646
- const trajectory = new TrajectoryLogger(query, classification);
6007
+ const trajectory = new TrajectoryLogger(query, classification, void 0, preselectedItemIds);
5647
6008
  let finalOutput;
5648
6009
  let executionError;
5649
6010
  try {
@@ -5656,7 +6017,9 @@ async function* executeV3({
5656
6017
  contextGuidance,
5657
6018
  customInstructions,
5658
6019
  classification,
5659
- onStepComplete: (step) => trajectory.recordStep(step)
6020
+ sessionId,
6021
+ onStepComplete: (step) => trajectory.recordStep(step),
6022
+ onTrajectoryStep: (data) => trajectory.recordRichStep(data)
5660
6023
  })) {
5661
6024
  finalOutput = output;
5662
6025
  yield output;
@@ -5667,7 +6030,7 @@ async function* executeV3({
5667
6030
  throw err;
5668
6031
  } finally {
5669
6032
  if (finalOutput) {
5670
- const trajectoryFile = await trajectory.finalize(finalOutput, !executionError, executionError);
6033
+ const trajectoryFile = await trajectory.finalize(finalOutput, !executionError, executionError, logTrajectory);
5671
6034
  if (trajectoryFile) {
5672
6035
  finalOutput.trajectoryFile = trajectoryFile;
5673
6036
  }
@@ -5680,7 +6043,8 @@ function createAgenticRetrievalToolV3({
5680
6043
  rerankers,
5681
6044
  user,
5682
6045
  role,
5683
- model
6046
+ model,
6047
+ preselectedItemIds
5684
6048
  }) {
5685
6049
  const license = checkLicense();
5686
6050
  if (!license["agentic-retrieval"]) {
@@ -5708,6 +6072,12 @@ function createAgenticRetrievalToolV3({
5708
6072
  type: "string",
5709
6073
  default: "none"
5710
6074
  },
6075
+ {
6076
+ name: "managed_context",
6077
+ description: "Makes sure the user defines which items from which contexts the agentic retrieval tool will search in",
6078
+ type: "boolean",
6079
+ default: false
6080
+ },
5711
6081
  {
5712
6082
  name: "reasoning_model",
5713
6083
  description: "By default the agentic retrieval tool uses the model from the agent calling the tool, but you can overwrite this here for the reasoning phase",
@@ -5720,6 +6090,18 @@ function createAgenticRetrievalToolV3({
5720
6090
  type: "string",
5721
6091
  default: ""
5722
6092
  },
6093
+ {
6094
+ name: "require_preselected_contexts",
6095
+ description: "Require the user to preselect contexts before executing the tool, meaning the user will be asked to select the contexts they want to search in",
6096
+ type: "boolean",
6097
+ default: false
6098
+ },
6099
+ {
6100
+ name: "log_trajectories",
6101
+ description: "Save a detailed markdown + JSON log of every retrieval execution to disk. Useful for debugging and evaluation.",
6102
+ type: "boolean",
6103
+ default: false
6104
+ },
5723
6105
  ...contexts.map((ctx) => ({
5724
6106
  name: ctx.id,
5725
6107
  description: `Enable search in "${ctx.name}". ${ctx.description}`,
@@ -5727,32 +6109,61 @@ function createAgenticRetrievalToolV3({
5727
6109
  default: true
5728
6110
  }))
5729
6111
  ],
5730
- inputSchema: z10.object({
5731
- query: z10.string().describe("The question or query to answer"),
5732
- userInstructions: z10.string().optional().describe("Additional instructions from the user to guide retrieval")
6112
+ inputSchema: z9.object({
6113
+ query: z9.string().describe("The question or query to answer"),
6114
+ userInstructions: z9.string().optional().describe("Additional instructions from the user to guide retrieval"),
6115
+ confirmedContextIds: z9.array(z9.string()).optional().describe(
6116
+ "Knowledge base IDs explicitly confirmed by the user to be used in the retrieval. When presen only searches these contexts. "
6117
+ )
5733
6118
  }),
5734
6119
  execute: async function* ({
5735
6120
  query,
5736
6121
  userInstructions,
5737
- toolVariablesConfig
6122
+ confirmedContextIds,
6123
+ toolVariablesConfig,
6124
+ sessionID
5738
6125
  }) {
5739
6126
  if (!model) {
5740
- throw new Error("Model is required for executing the agentic retrieval tool");
6127
+ yield { result: "Model is required for executing the agentic retrieval tool" };
6128
+ return;
5741
6129
  }
5742
6130
  let activeContexts = contexts;
5743
6131
  let configuredReranker;
5744
6132
  let configInstructions = "";
6133
+ let logTrajectory = false;
6134
+ let requiresPreselectedContexts = false;
6135
+ let managedContextEnabled = false;
5745
6136
  if (toolVariablesConfig) {
5746
6137
  configInstructions = toolVariablesConfig["instructions"] ?? "";
6138
+ logTrajectory = toolVariablesConfig["log_trajectories"] === true || toolVariablesConfig["log_trajectories"] === "true";
6139
+ managedContextEnabled = toolVariablesConfig["managed_context"] === true || toolVariablesConfig["managed_context"] === "true";
5747
6140
  activeContexts = contexts.filter(
5748
6141
  (ctx) => toolVariablesConfig[ctx.id] === true || toolVariablesConfig[ctx.id] === "true" || toolVariablesConfig[ctx.id] === 1
5749
6142
  );
5750
6143
  if (activeContexts.length === 0) activeContexts = contexts;
6144
+ requiresPreselectedContexts = toolVariablesConfig["require_preselected_contexts"] === true || toolVariablesConfig["require_preselected_contexts"] === "true";
5751
6145
  const rerankerId = toolVariablesConfig["reranker"];
5752
6146
  if (rerankerId && rerankerId !== "none") {
5753
6147
  configuredReranker = rerankers.find((r) => r.id === rerankerId);
5754
6148
  }
5755
6149
  }
6150
+ console.log("[EXULU] Managed context enabled:", managedContextEnabled);
6151
+ console.log("[EXULU] Preselected item IDs:", preselectedItemIds);
6152
+ if (managedContextEnabled && !preselectedItemIds?.length) {
6153
+ console.log("[EXULU] Managed context was enabled for the agentic retrieval tool. This means that the user must preselect items that the agentic retrieval tool will search in, please notify the user to preselect items before executing the tool.");
6154
+ yield { result: "Managed context was enabled for the agentic retrieval tool. This means that the user must preselect items that the agentic retrieval tool will search in, please notify the user to preselect items before executing the tool." };
6155
+ return;
6156
+ }
6157
+ if (requiresPreselectedContexts && !confirmedContextIds?.length && !preselectedItemIds?.length) {
6158
+ console.log("[EXULU] The user must choose between the available contexts before executing the tool. The available contexts are: " + activeContexts.map((c) => c.id).join(", ") + ". If the question_ask tool is available use that to ask the user which contexts they want to search in, otherwise just ask them in plain text.");
6159
+ yield { result: "The user must choose between the available contexts before executing the tool, the available contexts are: " + activeContexts.map((c) => c.id).join(", ") + ". If the question_ask tool is available use that to ask the user which contexts they want to search in, otherwise just ask them in plain text." };
6160
+ return;
6161
+ }
6162
+ if (confirmedContextIds?.length) {
6163
+ const confirmed = new Set(confirmedContextIds);
6164
+ const filtered = activeContexts.filter((c) => confirmed.has(c.id));
6165
+ if (filtered.length > 0) activeContexts = filtered;
6166
+ }
5756
6167
  const combinedInstructions = [
5757
6168
  configInstructions ? `Configuration instructions: ${configInstructions}` : "",
5758
6169
  adminInstructions ? `Admin instructions: ${adminInstructions}` : "",
@@ -5765,10 +6176,14 @@ function createAgenticRetrievalToolV3({
5765
6176
  model,
5766
6177
  user,
5767
6178
  role,
5768
- customInstructions: combinedInstructions || void 0
6179
+ customInstructions: combinedInstructions || void 0,
6180
+ logTrajectory,
6181
+ sessionId: sessionID,
6182
+ preselectedItemIds
5769
6183
  })) {
5770
6184
  yield { result: JSON.stringify(output) };
5771
6185
  }
6186
+ return;
5772
6187
  }
5773
6188
  });
5774
6189
  }
@@ -5923,9 +6338,9 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
5923
6338
  if (result.tools) {
5924
6339
  result.tools = await Promise.all(
5925
6340
  result.tools.map(
5926
- async (tool6) => {
6341
+ async (tool5) => {
5927
6342
  let hydrated;
5928
- if (tool6.id === "agentic_context_search") {
6343
+ if (tool5.id === "agentic_context_search") {
5929
6344
  const instance2 = createAgenticRetrievalToolV3({
5930
6345
  contexts: [],
5931
6346
  rerankers: [],
@@ -5941,23 +6356,23 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
5941
6356
  name: instance2.name,
5942
6357
  description: instance2.description,
5943
6358
  category: instance2.category,
5944
- config: tool6.config
6359
+ config: tool5.config
5945
6360
  };
5946
6361
  }
5947
- if (tool6.type === "agent") {
5948
- if (tool6.id === result.id) {
6362
+ if (tool5.type === "agent") {
6363
+ if (tool5.id === result.id) {
5949
6364
  return null;
5950
6365
  }
5951
- const instance2 = await exuluApp.get().agent(tool6.id);
6366
+ const instance2 = await exuluApp.get().agent(tool5.id);
5952
6367
  if (!instance2) {
5953
6368
  throw new Error(
5954
- "Trying to load a tool of type 'agent', but the associated agent with id " + tool6.id + " was not found in the database."
6369
+ "Trying to load a tool of type 'agent', but the associated agent with id " + tool5.id + " was not found in the database."
5955
6370
  );
5956
6371
  }
5957
6372
  const provider2 = providers.find((a) => a.id === instance2.provider);
5958
6373
  if (!provider2) {
5959
6374
  throw new Error(
5960
- "Trying to load a tool of type 'agent', but the associated agent with id " + tool6.id + " does not have a provider set for it."
6375
+ "Trying to load a tool of type 'agent', but the associated agent with id " + tool5.id + " does not have a provider set for it."
5961
6376
  );
5962
6377
  }
5963
6378
  const hasAccessToAgent = await checkRecordAccess(instance2, "read", user);
@@ -5966,13 +6381,13 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
5966
6381
  }
5967
6382
  hydrated = await provider2.tool(instance2.id, providers, contexts, rerankers);
5968
6383
  } else {
5969
- hydrated = tools.find((t) => t.id === tool6.id);
6384
+ hydrated = tools.find((t) => t.id === tool5.id);
5970
6385
  }
5971
6386
  const hydratedTool = {
5972
- ...tool6,
6387
+ ...tool5,
5973
6388
  name: hydrated?.name || "",
5974
6389
  description: hydrated?.description || "",
5975
- category: tool6?.category || "default"
6390
+ category: tool5?.category || "default"
5976
6391
  };
5977
6392
  console.log("[EXULU] hydratedTool", hydratedTool);
5978
6393
  return hydratedTool;
@@ -5990,7 +6405,7 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
5990
6405
  result.tools.unshift(projectTool);
5991
6406
  }
5992
6407
  }
5993
- result.tools = result.tools.filter((tool6) => tool6 !== null);
6408
+ result.tools = result.tools.filter((tool5) => tool5 !== null);
5994
6409
  } else {
5995
6410
  result.tools = [];
5996
6411
  }
@@ -7496,7 +7911,7 @@ var getEnabledTools = async (agent, allExuluTools, allContexts, allRerankers, di
7496
7911
  }
7497
7912
  console.log("[EXULU] available tools", enabledTools?.length);
7498
7913
  console.log("[EXULU] disabled tools", disabledTools?.length);
7499
- enabledTools = enabledTools.filter((tool6) => !disabledTools.includes(tool6.id));
7914
+ enabledTools = enabledTools.filter((tool5) => !disabledTools.includes(tool5.id));
7500
7915
  return enabledTools;
7501
7916
  };
7502
7917
 
@@ -7641,7 +8056,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
7641
8056
  );
7642
8057
  if (attempt < retries) {
7643
8058
  const backoffMs = 500 * Math.pow(2, attempt - 1);
7644
- await new Promise((resolve4) => setTimeout(resolve4, backoffMs));
8059
+ await new Promise((resolve3) => setTimeout(resolve3, backoffMs));
7645
8060
  }
7646
8061
  }
7647
8062
  }
@@ -7844,7 +8259,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
7844
8259
  } = await validateWorkflowPayload(data, providers);
7845
8260
  const retries2 = 3;
7846
8261
  let attempts = 0;
7847
- const promise = new Promise(async (resolve4, reject) => {
8262
+ const promise = new Promise(async (resolve3, reject) => {
7848
8263
  while (attempts < retries2) {
7849
8264
  try {
7850
8265
  const messages2 = await processUiMessagesFlow({
@@ -7859,7 +8274,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
7859
8274
  config,
7860
8275
  variables: data.inputs
7861
8276
  });
7862
- resolve4(messages2);
8277
+ resolve3(messages2);
7863
8278
  break;
7864
8279
  } catch (error) {
7865
8280
  console.error(
@@ -7870,7 +8285,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
7870
8285
  if (attempts >= retries2) {
7871
8286
  reject(new Error(error instanceof Error ? error.message : String(error)));
7872
8287
  }
7873
- await new Promise((resolve5) => setTimeout((resolve6) => resolve6(true), 2e3));
8288
+ await new Promise((resolve4) => setTimeout((resolve5) => resolve5(true), 2e3));
7874
8289
  }
7875
8290
  }
7876
8291
  });
@@ -7920,7 +8335,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
7920
8335
  } = await validateEvalPayload(data, providers);
7921
8336
  const retries2 = 3;
7922
8337
  let attempts = 0;
7923
- const promise = new Promise(async (resolve4, reject) => {
8338
+ const promise = new Promise(async (resolve3, reject) => {
7924
8339
  while (attempts < retries2) {
7925
8340
  try {
7926
8341
  const messages2 = await processUiMessagesFlow({
@@ -7934,7 +8349,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
7934
8349
  tools,
7935
8350
  config
7936
8351
  });
7937
- resolve4(messages2);
8352
+ resolve3(messages2);
7938
8353
  break;
7939
8354
  } catch (error) {
7940
8355
  console.error(
@@ -7945,7 +8360,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
7945
8360
  if (attempts >= retries2) {
7946
8361
  reject(new Error(error instanceof Error ? error.message : String(error)));
7947
8362
  }
7948
- await new Promise((resolve5) => setTimeout((resolve6) => resolve6(true), 2e3));
8363
+ await new Promise((resolve4) => setTimeout((resolve5) => resolve5(true), 2e3));
7949
8364
  }
7950
8365
  }
7951
8366
  });
@@ -8420,7 +8835,7 @@ var pollJobResult = async ({
8420
8835
  attempts++;
8421
8836
  const job = await Job.fromId(queue.queue, jobId);
8422
8837
  if (!job) {
8423
- await new Promise((resolve4) => setTimeout((resolve5) => resolve5(true), 2e3));
8838
+ await new Promise((resolve3) => setTimeout((resolve4) => resolve4(true), 2e3));
8424
8839
  continue;
8425
8840
  }
8426
8841
  const elapsedTime = Date.now() - startTime;
@@ -8450,7 +8865,7 @@ var pollJobResult = async ({
8450
8865
  console.log(`[EXULU] eval function ${job.id} result: ${result}`);
8451
8866
  break;
8452
8867
  }
8453
- await new Promise((resolve4) => setTimeout(() => resolve4(true), 2e3));
8868
+ await new Promise((resolve3) => setTimeout(() => resolve3(true), 2e3));
8454
8869
  }
8455
8870
  return result;
8456
8871
  };
@@ -8558,7 +8973,7 @@ var processUiMessagesFlow = async ({
8558
8973
  label: agent.name,
8559
8974
  trigger: "agent"
8560
8975
  };
8561
- messageHistory = await new Promise(async (resolve4, reject) => {
8976
+ messageHistory = await new Promise(async (resolve3, reject) => {
8562
8977
  const startTime = Date.now();
8563
8978
  try {
8564
8979
  const result = await provider.generateStream({
@@ -8566,7 +8981,7 @@ var processUiMessagesFlow = async ({
8566
8981
  rerankers,
8567
8982
  agent,
8568
8983
  user,
8569
- approvedTools: tools.map((tool6) => "tool-" + sanitizeToolName(tool6.name)),
8984
+ approvedTools: tools.map((tool5) => "tool-" + sanitizeToolName(tool5.name)),
8570
8985
  instructions: agent.instructions,
8571
8986
  session: void 0,
8572
8987
  previousMessages: messageHistory.messages,
@@ -8635,7 +9050,7 @@ var processUiMessagesFlow = async ({
8635
9050
  })
8636
9051
  ] : []
8637
9052
  ]);
8638
- resolve4({
9053
+ resolve3({
8639
9054
  messages,
8640
9055
  metadata: {
8641
9056
  tokens: {
@@ -9383,7 +9798,7 @@ type PageInfo {
9383
9798
  } = await validateWorkflowPayload(jobData, providers);
9384
9799
  const retries = 3;
9385
9800
  let attempts = 0;
9386
- const promise = new Promise(async (resolve4, reject) => {
9801
+ const promise = new Promise(async (resolve3, reject) => {
9387
9802
  while (attempts < retries) {
9388
9803
  try {
9389
9804
  const messages2 = await processUiMessagesFlow({
@@ -9398,7 +9813,7 @@ type PageInfo {
9398
9813
  config,
9399
9814
  variables: args.variables
9400
9815
  });
9401
- resolve4(messages2);
9816
+ resolve3(messages2);
9402
9817
  break;
9403
9818
  } catch (error) {
9404
9819
  console.error(
@@ -9412,7 +9827,7 @@ type PageInfo {
9412
9827
  if (attempts >= retries) {
9413
9828
  reject(error instanceof Error ? error : new Error(String(error)));
9414
9829
  }
9415
- await new Promise((resolve5) => setTimeout((resolve6) => resolve6(true), 2e3));
9830
+ await new Promise((resolve4) => setTimeout((resolve5) => resolve5(true), 2e3));
9416
9831
  }
9417
9832
  }
9418
9833
  });
@@ -9665,10 +10080,10 @@ type PageInfo {
9665
10080
  contexts.map(async (context2) => {
9666
10081
  let processor = null;
9667
10082
  if (context2.processor) {
9668
- processor = await new Promise(async (resolve4, reject) => {
10083
+ processor = await new Promise(async (resolve3, reject) => {
9669
10084
  const config2 = context2.processor?.config;
9670
10085
  const queue = await config2?.queue;
9671
- resolve4({
10086
+ resolve3({
9672
10087
  name: context2.processor.name,
9673
10088
  description: context2.processor.description,
9674
10089
  queue: queue?.queue?.name || void 0,
@@ -9749,10 +10164,10 @@ type PageInfo {
9749
10164
  }
9750
10165
  let processor = null;
9751
10166
  if (data.processor) {
9752
- processor = await new Promise(async (resolve4, reject) => {
10167
+ processor = await new Promise(async (resolve3, reject) => {
9753
10168
  const config2 = data.processor?.config;
9754
10169
  const queue = await config2?.queue;
9755
- resolve4({
10170
+ resolve3({
9756
10171
  name: data.processor.name,
9757
10172
  description: data.processor.description,
9758
10173
  queue: queue?.queue?.name || void 0,
@@ -9841,7 +10256,7 @@ type PageInfo {
9841
10256
  })
9842
10257
  );
9843
10258
  let agenticRetrievalTool = void 0;
9844
- const filtered = agentTools.filter((tool6) => tool6 !== null);
10259
+ const filtered = agentTools.filter((tool5) => tool5 !== null);
9845
10260
  let allTools = [...filtered, ...tools];
9846
10261
  if (contexts?.length) {
9847
10262
  agenticRetrievalTool = createAgenticRetrievalToolV3({
@@ -9859,21 +10274,21 @@ type PageInfo {
9859
10274
  if (search && search.trim()) {
9860
10275
  const searchTerm = search.toLowerCase().trim();
9861
10276
  allTools = allTools.filter(
9862
- (tool6) => tool6.name?.toLowerCase().includes(searchTerm) || tool6.description?.toLowerCase().includes(searchTerm)
10277
+ (tool5) => tool5.name?.toLowerCase().includes(searchTerm) || tool5.description?.toLowerCase().includes(searchTerm)
9863
10278
  );
9864
10279
  }
9865
10280
  if (category && category.trim()) {
9866
- allTools = allTools.filter((tool6) => tool6.category === category);
10281
+ allTools = allTools.filter((tool5) => tool5.category === category);
9867
10282
  }
9868
10283
  const total = allTools.length;
9869
10284
  const start = page * limit;
9870
10285
  const end = start + limit;
9871
10286
  const paginatedTools = allTools.slice(start, end);
9872
10287
  return {
9873
- items: paginatedTools.map((tool6) => {
10288
+ items: paginatedTools.map((tool5) => {
9874
10289
  const object = {};
9875
10290
  requestedFields.forEach((field) => {
9876
- object[field] = tool6[field];
10291
+ object[field] = tool5[field];
9877
10292
  });
9878
10293
  return object;
9879
10294
  }),
@@ -9883,7 +10298,7 @@ type PageInfo {
9883
10298
  };
9884
10299
  };
9885
10300
  resolvers.Query["toolCategories"] = async () => {
9886
- const array = tools.map((tool6) => tool6.category).filter((category) => category && typeof category === "string");
10301
+ const array = tools.map((tool5) => tool5.category).filter((category) => category && typeof category === "string");
9887
10302
  array.push("contexts");
9888
10303
  array.push("agents");
9889
10304
  return [...new Set(array)].sort();
@@ -10246,10 +10661,10 @@ type AgentWorldAgent {
10246
10661
  import { expressMiddleware } from "@as-integrations/express5";
10247
10662
  import { InMemoryLRUCache } from "@apollo/utils.keyvaluecache";
10248
10663
  import bodyParser from "body-parser";
10249
- import CryptoJS6 from "crypto-js";
10664
+ import CryptoJS7 from "crypto-js";
10250
10665
  import OpenAI from "openai";
10251
- import fs4 from "fs";
10252
- import { randomUUID as randomUUID4 } from "crypto";
10666
+ import fs3 from "fs";
10667
+ import { randomUUID as randomUUID5 } from "crypto";
10253
10668
  import "@opentelemetry/api";
10254
10669
  import Anthropic from "@anthropic-ai/sdk";
10255
10670
 
@@ -10283,7 +10698,7 @@ import { createIdGenerator } from "ai";
10283
10698
  import cookieParser from "cookie-parser";
10284
10699
 
10285
10700
  // src/exulu/provider.ts
10286
- import { z as z11 } from "zod";
10701
+ import { z as z10 } from "zod";
10287
10702
  import {
10288
10703
  convertToModelMessages,
10289
10704
  Output as Output2,
@@ -10348,7 +10763,7 @@ async function clearSessionCurrentTask(session) {
10348
10763
  }
10349
10764
 
10350
10765
  // src/exulu/provider.ts
10351
- import fs3 from "fs";
10766
+ import fs2 from "fs";
10352
10767
  var ExuluProvider = class {
10353
10768
  // Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
10354
10769
  // underscores and be a max length of 80 characters and at least 5 characters long.
@@ -10430,9 +10845,9 @@ var ExuluProvider = class {
10430
10845
  name: `${agent.name}`,
10431
10846
  type: "agent",
10432
10847
  category: "agents",
10433
- inputSchema: z11.object({
10434
- prompt: z11.string().describe("The prompt (usually a question for the agent) to send to the agent."),
10435
- information: z11.string().describe("A summary of relevant context / information from the current session")
10848
+ inputSchema: z10.object({
10849
+ prompt: z10.string().describe("The prompt (usually a question for the agent) to send to the agent."),
10850
+ information: z10.string().describe("A summary of relevant context / information from the current session")
10436
10851
  }),
10437
10852
  description: `This tool calls an agent named: ${agent.name}. The agent does the following: ${agent.description}.`,
10438
10853
  config: [],
@@ -10635,12 +11050,12 @@ var ExuluProvider = class {
10635
11050
  system += "\n\n" + memoryContext;
10636
11051
  }
10637
11052
  const includesContextSearchTool = currentTools?.some(
10638
- (tool6) => tool6.name.toLowerCase().includes("context_search") || tool6.id.includes("context_search") || tool6.type === "context"
11053
+ (tool5) => tool5.name.toLowerCase().includes("context_search") || tool5.id.includes("context_search") || tool5.type === "context"
10639
11054
  );
10640
11055
  const includesWebSearchTool = currentTools?.some(
10641
- (tool6) => tool6.name.toLowerCase().includes("web_search") || tool6.id.includes("web_search") || tool6.type === "web_search"
11056
+ (tool5) => tool5.name.toLowerCase().includes("web_search") || tool5.id.includes("web_search") || tool5.type === "web_search"
10642
11057
  );
10643
- console.log("[EXULU] Current tools: " + currentTools?.map((tool6) => tool6.name).join("\n"));
11058
+ console.log("[EXULU] Current tools: " + currentTools?.map((tool5) => tool5.name).join("\n"));
10644
11059
  console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
10645
11060
  if (includesContextSearchTool) {
10646
11061
  system += `
@@ -11013,7 +11428,7 @@ ${extractedText}
11013
11428
  // todo make this configurable?
11014
11429
  page: 1
11015
11430
  });
11016
- fs3.writeFileSync("pre-fetched-relevant-information.json", JSON.stringify(result2, null, 2));
11431
+ fs2.writeFileSync("pre-fetched-relevant-information.json", JSON.stringify(result2, null, 2));
11017
11432
  if (result2?.chunks?.length) {
11018
11433
  memoryContext = `
11019
11434
  <pre-fetched relevant information for this query>:
@@ -11038,12 +11453,12 @@ ${extractedText}
11038
11453
  system += "\n\n" + memoryContext;
11039
11454
  }
11040
11455
  const includesContextSearchTool = currentTools?.some(
11041
- (tool6) => tool6.name.toLowerCase().includes("context_search") || tool6.id.includes("context_search") || tool6.type === "context"
11456
+ (tool5) => tool5.name.toLowerCase().includes("context_search") || tool5.id.includes("context_search") || tool5.type === "context"
11042
11457
  );
11043
11458
  const includesWebSearchTool = currentTools?.some(
11044
- (tool6) => tool6.name.toLowerCase().includes("web_search") || tool6.id.includes("web_search") || tool6.type === "web_search"
11459
+ (tool5) => tool5.name.toLowerCase().includes("web_search") || tool5.id.includes("web_search") || tool5.type === "web_search"
11045
11460
  );
11046
- console.log("[EXULU] Current tools: " + currentTools?.map((tool6) => tool6.name).join("\n"));
11461
+ console.log("[EXULU] Current tools: " + currentTools?.map((tool5) => tool5.name).join("\n"));
11047
11462
  console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
11048
11463
  console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
11049
11464
  if (includesContextSearchTool) {
@@ -11088,7 +11503,7 @@ ${extractedText}
11088
11503
 
11089
11504
  When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
11090
11505
  'Inform the user that the action was not performed.`;
11091
- fs3.writeFileSync("system-prompt.txt", system);
11506
+ fs2.writeFileSync("system-prompt.txt", system);
11092
11507
  const result = streamText({
11093
11508
  temperature: 0,
11094
11509
  // TODO Make this configurable
@@ -11233,13 +11648,462 @@ var providerRateLimiter = async (key, windowSeconds, limit, points) => {
11233
11648
  }
11234
11649
  };
11235
11650
 
11651
+ // src/exulu/openai-gateway.ts
11652
+ import "express";
11653
+ import {
11654
+ streamText as streamText2,
11655
+ generateText as generateText5,
11656
+ stepCountIs as stepCountIs3
11657
+ } from "ai";
11658
+ import { randomUUID as randomUUID4 } from "crypto";
11659
+ import CryptoJS6 from "crypto-js";
11660
+ import express from "express";
11661
+ function convertOpenAIMessagesToCoreMessages(messages) {
11662
+ const systemParts = [];
11663
+ const coreMessages = [];
11664
+ for (const msg of messages) {
11665
+ if (msg.role === "system") {
11666
+ systemParts.push(typeof msg.content === "string" ? msg.content : "");
11667
+ continue;
11668
+ }
11669
+ if (msg.role === "user") {
11670
+ if (typeof msg.content === "string") {
11671
+ coreMessages.push({ role: "user", content: msg.content });
11672
+ } else if (Array.isArray(msg.content)) {
11673
+ const parts = msg.content.flatMap((part) => {
11674
+ if (part.type === "text") return [{ type: "text", text: part.text }];
11675
+ if (part.type === "image_url") return [{ type: "image", image: part.image_url.url }];
11676
+ return [];
11677
+ });
11678
+ coreMessages.push({ role: "user", content: parts });
11679
+ }
11680
+ continue;
11681
+ }
11682
+ if (msg.role === "assistant") {
11683
+ if (msg.tool_calls && msg.tool_calls.length > 0) {
11684
+ const parts = [];
11685
+ if (typeof msg.content === "string" && msg.content) {
11686
+ parts.push({ type: "text", text: msg.content });
11687
+ }
11688
+ for (const tc of msg.tool_calls) {
11689
+ parts.push({
11690
+ type: "tool-call",
11691
+ toolCallId: tc.id,
11692
+ toolName: tc.function.name,
11693
+ args: JSON.parse(tc.function.arguments || "{}")
11694
+ });
11695
+ }
11696
+ coreMessages.push({ role: "assistant", content: parts });
11697
+ } else {
11698
+ coreMessages.push({
11699
+ role: "assistant",
11700
+ content: typeof msg.content === "string" ? msg.content : ""
11701
+ });
11702
+ }
11703
+ continue;
11704
+ }
11705
+ if (msg.role === "tool") {
11706
+ coreMessages.push({
11707
+ role: "tool",
11708
+ content: [
11709
+ {
11710
+ type: "tool-result",
11711
+ toolCallId: msg.tool_call_id ?? "",
11712
+ result: msg.content
11713
+ }
11714
+ ]
11715
+ });
11716
+ }
11717
+ }
11718
+ return { systemPrompt: systemParts.join("\n\n"), coreMessages };
11719
+ }
11720
+ async function writeStatistics(agent, project, user, inputTokens, outputTokens) {
11721
+ const label = agent.name;
11722
+ const trigger = "agent";
11723
+ const projectId = project?.id ? { project: project.id } : {};
11724
+ await Promise.all([
11725
+ updateStatistic({
11726
+ name: "count",
11727
+ label,
11728
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
11729
+ trigger,
11730
+ count: 1,
11731
+ user: user.id,
11732
+ role: user.role?.id,
11733
+ ...projectId
11734
+ }),
11735
+ ...inputTokens ? [
11736
+ updateStatistic({
11737
+ name: "inputTokens",
11738
+ label,
11739
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
11740
+ trigger,
11741
+ count: inputTokens,
11742
+ user: user.id,
11743
+ role: user.role?.id,
11744
+ ...projectId
11745
+ })
11746
+ ] : [],
11747
+ ...outputTokens ? [
11748
+ updateStatistic({
11749
+ name: "outputTokens",
11750
+ label,
11751
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
11752
+ trigger,
11753
+ count: outputTokens,
11754
+ user: user.id,
11755
+ role: user.role?.id,
11756
+ ...projectId
11757
+ })
11758
+ ] : []
11759
+ ]);
11760
+ }
11761
+ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config, rerankers) => {
11762
+ const { agentsSchema: agentsSchema4, projectsSchema: projectsSchema4 } = coreSchemas.get();
11763
+ app.get(
11764
+ "/gateway/open-ai/v1/models",
11765
+ async (req, res) => {
11766
+ try {
11767
+ const authResult = await requestValidators.authenticate(req);
11768
+ if (!authResult.user?.id) {
11769
+ res.status(authResult.code || 401).json({ error: { message: authResult.message, type: "authentication_error" } });
11770
+ return;
11771
+ }
11772
+ const { db: db2 } = await postgresClient();
11773
+ let projectsQuery = db2("projects").select("id", "name");
11774
+ projectsQuery = applyAccessControl(projectsSchema4(), projectsQuery, authResult.user);
11775
+ const projects = await projectsQuery;
11776
+ let agentsQuery = db2("agents").select("id", "name");
11777
+ agentsQuery = applyAccessControl(agentsSchema4(), agentsQuery, authResult.user);
11778
+ const agents = await agentsQuery;
11779
+ const data = projects.flatMap(
11780
+ (p) => agents.map((a) => ({
11781
+ id: `${p.name}/${a.name}`,
11782
+ object: "model",
11783
+ created: 0,
11784
+ owned_by: "exulu"
11785
+ }))
11786
+ );
11787
+ res.json({ object: "list", data });
11788
+ } catch (error) {
11789
+ console.error("[OPENAI GATEWAY] /v1/models error:", error);
11790
+ res.status(500).json({ error: { message: error.message, type: "server_error" } });
11791
+ }
11792
+ }
11793
+ );
11794
+ app.get(
11795
+ "/gateway/open-ai/v1/models/:projectId/:agentId",
11796
+ async (req, res) => {
11797
+ try {
11798
+ const authResult = await requestValidators.authenticate(req);
11799
+ if (!authResult.user?.id) {
11800
+ res.status(authResult.code || 401).json({ error: { message: authResult.message, type: "authentication_error" } });
11801
+ return;
11802
+ }
11803
+ const { db: db2 } = await postgresClient();
11804
+ let projectQuery = db2("projects").select("id", "name");
11805
+ projectQuery = applyAccessControl(projectsSchema4(), projectQuery, authResult.user);
11806
+ projectQuery.where({ id: req.params.projectId });
11807
+ const project = await projectQuery.first();
11808
+ let agentQuery = db2("agents").select("id", "name");
11809
+ agentQuery = applyAccessControl(agentsSchema4(), agentQuery, authResult.user);
11810
+ agentQuery.where({ id: req.params.agentId });
11811
+ const agent = await agentQuery.first();
11812
+ if (!project || !agent) {
11813
+ res.status(404).json({ error: { message: "Model not found", type: "invalid_request_error" } });
11814
+ return;
11815
+ }
11816
+ res.json({
11817
+ id: `${project.name}/${agent.name}`,
11818
+ object: "model",
11819
+ created: 0,
11820
+ owned_by: "exulu"
11821
+ });
11822
+ } catch (error) {
11823
+ console.error("[OPENAI GATEWAY] /v1/models/:id error:", error);
11824
+ res.status(500).json({ error: { message: error.message, type: "server_error" } });
11825
+ }
11826
+ }
11827
+ );
11828
+ app.post(
11829
+ "/gateway/open-ai/v1/chat/completions",
11830
+ express.json({ limit: REQUEST_SIZE_LIMIT }),
11831
+ async (req, res) => {
11832
+ try {
11833
+ const { db: db2 } = await postgresClient();
11834
+ const authResult = await requestValidators.authenticate(req);
11835
+ if (!authResult.user?.id) {
11836
+ res.status(authResult.code || 401).json({ error: { message: authResult.message, type: "authentication_error" } });
11837
+ return;
11838
+ }
11839
+ const user = authResult.user;
11840
+ const modelId = req.body.model;
11841
+ if (!modelId) {
11842
+ res.status(400).json({
11843
+ error: { message: "Missing required field: model", type: "invalid_request_error" }
11844
+ });
11845
+ return;
11846
+ }
11847
+ const separatorIndex = modelId.indexOf("/");
11848
+ if (separatorIndex === -1) {
11849
+ res.status(400).json({
11850
+ error: { message: "Invalid model format. Expected: 'projectname/agentname'", type: "invalid_request_error" }
11851
+ });
11852
+ return;
11853
+ }
11854
+ const projectName = modelId.substring(0, separatorIndex);
11855
+ const agentName = modelId.substring(separatorIndex + 1);
11856
+ let agentQuery = db2("agents").select("*");
11857
+ agentQuery = applyAccessControl(agentsSchema4(), agentQuery, user);
11858
+ agentQuery.where({ name: agentName });
11859
+ const agent = await agentQuery.first();
11860
+ if (!agent) {
11861
+ res.status(404).json({
11862
+ error: {
11863
+ message: `Agent '${agentName}' not found or you do not have access to it.`,
11864
+ type: "invalid_request_error"
11865
+ }
11866
+ });
11867
+ return;
11868
+ }
11869
+ let project = null;
11870
+ if (projectName) {
11871
+ let projectQuery = db2("projects").select("*");
11872
+ projectQuery = applyAccessControl(projectsSchema4(), projectQuery, user);
11873
+ projectQuery.where({ name: projectName });
11874
+ project = await projectQuery.first();
11875
+ }
11876
+ if (!process.env.NEXTAUTH_SECRET) {
11877
+ res.status(500).json({ error: { message: "Server configuration error", type: "server_error" } });
11878
+ return;
11879
+ }
11880
+ if (!agent.providerapikey) {
11881
+ res.status(400).json({
11882
+ error: { message: "Agent has no API key configured", type: "invalid_request_error" }
11883
+ });
11884
+ return;
11885
+ }
11886
+ const variable = await db2.from("variables").where({ name: agent.providerapikey }).first();
11887
+ if (!variable) {
11888
+ res.status(400).json({
11889
+ error: { message: "API key variable not found", type: "invalid_request_error" }
11890
+ });
11891
+ return;
11892
+ }
11893
+ if (!variable.encrypted) {
11894
+ res.status(400).json({
11895
+ error: { message: "API key variable must be encrypted", type: "invalid_request_error" }
11896
+ });
11897
+ return;
11898
+ }
11899
+ const bytes = CryptoJS6.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
11900
+ const providerapikey = bytes.toString(CryptoJS6.enc.Utf8);
11901
+ const provider = providers.find((p) => p.id === agent.provider);
11902
+ if (!provider?.config?.model?.create) {
11903
+ res.status(400).json({
11904
+ error: { message: "No provider configured for this agent", type: "invalid_request_error" }
11905
+ });
11906
+ return;
11907
+ }
11908
+ const languageModel = provider.config.model.create({ apiKey: providerapikey });
11909
+ const disabledTools = req.body.disabledTools ?? [];
11910
+ const enabledTools = await getEnabledTools(
11911
+ agent,
11912
+ tools,
11913
+ contexts ?? [],
11914
+ rerankers ?? [],
11915
+ disabledTools,
11916
+ providers,
11917
+ user
11918
+ );
11919
+ const convertedTools = await convertExuluToolsToAiSdkTools(
11920
+ enabledTools,
11921
+ [],
11922
+ tools,
11923
+ agent.tools,
11924
+ providerapikey,
11925
+ contexts,
11926
+ rerankers,
11927
+ user,
11928
+ config,
11929
+ void 0,
11930
+ req,
11931
+ project?.id,
11932
+ void 0,
11933
+ languageModel,
11934
+ agent
11935
+ );
11936
+ const openaiMessages = req.body.messages ?? [];
11937
+ const { systemPrompt: requestSystemPrompt, coreMessages } = convertOpenAIMessagesToCoreMessages(openaiMessages);
11938
+ const agentInstructions = agent.instructions ?? "";
11939
+ const systemParts = [
11940
+ agentInstructions ? `You are an agent named: ${agent.name}
11941
+ Here are your instructions: ${agentInstructions}` : `You are an agent named: ${agent.name}`,
11942
+ project ? `The project you are working on is: ${project.name}${project.description ? `
11943
+ ${project.description}` : ""}` : "",
11944
+ requestSystemPrompt
11945
+ ].filter(Boolean);
11946
+ const systemPrompt = systemParts.join("\n\n");
11947
+ const completionId = `chatcmpl-${randomUUID4()}`;
11948
+ const created = Math.floor(Date.now() / 1e3);
11949
+ const hasTools = Object.keys(convertedTools).length > 0;
11950
+ if (req.body.stream === true) {
11951
+ res.setHeader("Content-Type", "text/event-stream");
11952
+ res.setHeader("Cache-Control", "no-cache");
11953
+ res.setHeader("Connection", "keep-alive");
11954
+ const result = streamText2({
11955
+ model: languageModel,
11956
+ system: systemPrompt || void 0,
11957
+ messages: coreMessages,
11958
+ tools: hasTools ? convertedTools : void 0,
11959
+ maxRetries: 2,
11960
+ stopWhen: [stepCountIs3(5)],
11961
+ onError: (error) => {
11962
+ console.error("[OPENAI GATEWAY] stream error:", error);
11963
+ }
11964
+ });
11965
+ res.write(
11966
+ `data: ${JSON.stringify({
11967
+ id: completionId,
11968
+ object: "chat.completion.chunk",
11969
+ created,
11970
+ model: modelId,
11971
+ choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }]
11972
+ })}
11973
+
11974
+ `
11975
+ );
11976
+ let inputTokens = 0;
11977
+ let outputTokens = 0;
11978
+ for await (const chunk of result.fullStream) {
11979
+ if (chunk.type === "text-delta") {
11980
+ res.write(
11981
+ `data: ${JSON.stringify({
11982
+ id: completionId,
11983
+ object: "chat.completion.chunk",
11984
+ created,
11985
+ model: modelId,
11986
+ choices: [{ index: 0, delta: { content: chunk.text }, finish_reason: null }]
11987
+ })}
11988
+
11989
+ `
11990
+ );
11991
+ } else if (chunk.type === "tool-input-start") {
11992
+ res.write(
11993
+ `data: ${JSON.stringify({
11994
+ id: completionId,
11995
+ object: "chat.completion.chunk",
11996
+ created,
11997
+ model: modelId,
11998
+ choices: [
11999
+ {
12000
+ index: 0,
12001
+ delta: {
12002
+ tool_calls: [
12003
+ {
12004
+ index: 0,
12005
+ id: chunk.id,
12006
+ type: "function",
12007
+ function: { name: chunk.toolName, arguments: "" }
12008
+ }
12009
+ ]
12010
+ },
12011
+ finish_reason: null
12012
+ }
12013
+ ]
12014
+ })}
12015
+
12016
+ `
12017
+ );
12018
+ } else if (chunk.type === "tool-input-delta") {
12019
+ res.write(
12020
+ `data: ${JSON.stringify({
12021
+ id: completionId,
12022
+ object: "chat.completion.chunk",
12023
+ created,
12024
+ model: modelId,
12025
+ choices: [
12026
+ {
12027
+ index: 0,
12028
+ delta: { tool_calls: [{ index: 0, function: { arguments: chunk.delta } }] },
12029
+ finish_reason: null
12030
+ }
12031
+ ]
12032
+ })}
12033
+
12034
+ `
12035
+ );
12036
+ } else if (chunk.type === "finish") {
12037
+ inputTokens = chunk.usage?.inputTokens ?? 0;
12038
+ outputTokens = chunk.usage?.outputTokens ?? 0;
12039
+ const finishReason = chunk.finishReason === "tool-calls" ? "tool_calls" : "stop";
12040
+ res.write(
12041
+ `data: ${JSON.stringify({
12042
+ id: completionId,
12043
+ object: "chat.completion.chunk",
12044
+ created,
12045
+ model: modelId,
12046
+ choices: [{ index: 0, delta: {}, finish_reason: finishReason }],
12047
+ usage: {
12048
+ prompt_tokens: inputTokens,
12049
+ completion_tokens: outputTokens,
12050
+ total_tokens: inputTokens + outputTokens
12051
+ }
12052
+ })}
12053
+
12054
+ `
12055
+ );
12056
+ }
12057
+ }
12058
+ res.write("data: [DONE]\n\n");
12059
+ res.end();
12060
+ await writeStatistics(agent, project, user, inputTokens, outputTokens);
12061
+ } else {
12062
+ const { text, usage } = await generateText5({
12063
+ model: languageModel,
12064
+ system: systemPrompt || void 0,
12065
+ messages: coreMessages,
12066
+ tools: hasTools ? convertedTools : void 0,
12067
+ maxRetries: 2,
12068
+ stopWhen: [stepCountIs3(5)]
12069
+ });
12070
+ res.json({
12071
+ id: completionId,
12072
+ object: "chat.completion",
12073
+ created,
12074
+ model: agentId,
12075
+ choices: [
12076
+ {
12077
+ index: 0,
12078
+ message: { role: "assistant", content: text },
12079
+ finish_reason: "stop"
12080
+ }
12081
+ ],
12082
+ usage: {
12083
+ prompt_tokens: usage.promptTokens,
12084
+ completion_tokens: usage.completionTokens,
12085
+ total_tokens: usage.totalTokens
12086
+ }
12087
+ });
12088
+ await writeStatistics(agent, project, user, usage.promptTokens, usage.completionTokens);
12089
+ }
12090
+ } catch (error) {
12091
+ console.error("[OPENAI GATEWAY] /v1/chat/completions error:", error);
12092
+ if (!res.headersSent) {
12093
+ res.status(500).json({ error: { message: error.message, type: "server_error" } });
12094
+ }
12095
+ }
12096
+ }
12097
+ );
12098
+ };
12099
+
11236
12100
  // src/exulu/routes.ts
11237
12101
  import { convertJsonSchemaToZod } from "zod-from-json-schema";
11238
12102
  var REQUEST_SIZE_LIMIT = "50mb";
11239
12103
  var getExuluVersionNumber = async () => {
11240
12104
  try {
11241
- const path5 = process.cwd();
11242
- const packageJson = fs4.readFileSync(path5 + "/package.json", "utf8");
12105
+ const path3 = process.cwd();
12106
+ const packageJson = fs3.readFileSync(path3 + "/package.json", "utf8");
11243
12107
  const packageData = JSON.parse(packageJson);
11244
12108
  const exuluVersion = packageData.dependencies["@exulu/backend"];
11245
12109
  console.log(`[EXULU] Installed exulu-backend version: ${exuluVersion}`);
@@ -11264,6 +12128,7 @@ var {
11264
12128
  agentMessagesSchema: agentMessagesSchema2,
11265
12129
  rolesSchema: rolesSchema2,
11266
12130
  usersSchema: usersSchema2,
12131
+ skillsSchema: skillsSchema2,
11267
12132
  variablesSchema: variablesSchema2,
11268
12133
  workflowTemplatesSchema: workflowTemplatesSchema2,
11269
12134
  rbacSchema: rbacSchema2,
@@ -11283,7 +12148,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
11283
12148
  if (tracer) {
11284
12149
  console.log("[EXULU] tracer configured", tracer);
11285
12150
  }
11286
- app.use(express.json({ limit: REQUEST_SIZE_LIMIT }));
12151
+ app.use(express2.json({ limit: REQUEST_SIZE_LIMIT }));
11287
12152
  app.use(cors(corsOptions));
11288
12153
  app.use(bodyParser.urlencoded({ extended: true, limit: REQUEST_SIZE_LIMIT }));
11289
12154
  app.use(bodyParser.json({ limit: REQUEST_SIZE_LIMIT }));
@@ -11308,6 +12173,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
11308
12173
  const schema = createSDL(
11309
12174
  [
11310
12175
  usersSchema2(),
12176
+ skillsSchema2(),
11311
12177
  rolesSchema2(),
11312
12178
  agentsSchema2(),
11313
12179
  feedbackSchema2(),
@@ -11343,7 +12209,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
11343
12209
  app.use(
11344
12210
  "/graphql",
11345
12211
  cors(corsOptions),
11346
- express.json({ limit: REQUEST_SIZE_LIMIT }),
12212
+ express2.json({ limit: REQUEST_SIZE_LIMIT }),
11347
12213
  expressMiddleware(server, {
11348
12214
  context: async ({ req }) => {
11349
12215
  const authenticationResult = await requestValidators.authenticate(req);
@@ -11465,8 +12331,8 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
11465
12331
  return;
11466
12332
  }
11467
12333
  if (variable.encrypted) {
11468
- const bytes = CryptoJS6.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
11469
- providerapikey = bytes.toString(CryptoJS6.enc.Utf8);
12334
+ const bytes = CryptoJS7.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
12335
+ providerapikey = bytes.toString(CryptoJS7.enc.Utf8);
11470
12336
  }
11471
12337
  const openai = new OpenAI({
11472
12338
  apiKey: providerapikey
@@ -11515,7 +12381,7 @@ Mood: friendly and intelligent.
11515
12381
  });
11516
12382
  return;
11517
12383
  }
11518
- const uuid = randomUUID4();
12384
+ const uuid = randomUUID5();
11519
12385
  const image_url = await uploadFile(Buffer.from(image_base64, "base64"), `${uuid}.png`, config, {
11520
12386
  contentType: "image/png"
11521
12387
  }, authenticationResult.user?.id, void 0, true);
@@ -11684,8 +12550,8 @@ Mood: friendly and intelligent.
11684
12550
  return;
11685
12551
  }
11686
12552
  if (variable.encrypted) {
11687
- const bytes = CryptoJS6.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
11688
- providerapikey = bytes.toString(CryptoJS6.enc.Utf8);
12553
+ const bytes = CryptoJS7.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
12554
+ providerapikey = bytes.toString(CryptoJS7.enc.Utf8);
11689
12555
  }
11690
12556
  }
11691
12557
  if (!!headers.stream) {
@@ -11862,7 +12728,7 @@ ${customInstructions}` : agent.instructions;
11862
12728
  });
11863
12729
  app.use(
11864
12730
  "/gateway/anthropic/:agent/:project",
11865
- express.raw({ type: "*/*", limit: REQUEST_SIZE_LIMIT }),
12731
+ express2.raw({ type: "*/*", limit: REQUEST_SIZE_LIMIT }),
11866
12732
  async (req, res) => {
11867
12733
  try {
11868
12734
  if (!req.body.tools) {
@@ -11946,8 +12812,8 @@ ${customInstructions}` : agent.instructions;
11946
12812
  return;
11947
12813
  }
11948
12814
  if (variable.encrypted) {
11949
- const bytes = CryptoJS6.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
11950
- anthropicApiKey = bytes.toString(CryptoJS6.enc.Utf8);
12815
+ const bytes = CryptoJS7.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
12816
+ anthropicApiKey = bytes.toString(CryptoJS7.enc.Utf8);
11951
12817
  }
11952
12818
  const headers = {
11953
12819
  "x-api-key": anthropicApiKey,
@@ -12026,14 +12892,14 @@ ${customInstructions}` : agent?.instructions;
12026
12892
  console.log("[EXULU] Using tool", toolName);
12027
12893
  const inputs = event.message?.input;
12028
12894
  const id = event.message?.id;
12029
- const tool6 = enabledTools.find(
12030
- (tool7) => tool7.id === toolName.replace("exulu_", "")
12895
+ const tool5 = enabledTools.find(
12896
+ (tool6) => tool6.id === toolName.replace("exulu_", "")
12031
12897
  );
12032
- if (!tool6 || !tool6.tool.execute) {
12898
+ if (!tool5 || !tool5.tool.execute) {
12033
12899
  console.error("[EXULU] Tool not found or not enabled.", toolName);
12034
12900
  continue;
12035
12901
  }
12036
- const toolResult = await tool6.tool.execute(inputs, {
12902
+ const toolResult = await tool5.tool.execute(inputs, {
12037
12903
  toolCallId: id,
12038
12904
  messages: [
12039
12905
  {
@@ -12125,35 +12991,439 @@ data: ${JSON.stringify(event)}
12125
12991
  }
12126
12992
  }
12127
12993
  );
12128
- app.use(express.static("public"));
12129
- return app;
12130
- };
12131
- var createCustomAnthropicStreamingMessage = (message) => {
12132
- const responseData = {
12133
- type: "message",
12134
- content: [
12135
- {
12136
- type: "text",
12137
- text: message
12994
+ function buildFileTree(files, stripPrefix) {
12995
+ const root = { name: "/", path: "/", key: "", type: "folder", children: [] };
12996
+ for (const file of files) {
12997
+ const relativePath = file.key.startsWith(stripPrefix) ? file.key.slice(stripPrefix.length) : file.key;
12998
+ const parts = relativePath.split("/").filter(Boolean);
12999
+ let current = root;
13000
+ for (let i = 0; i < parts.length; i++) {
13001
+ const part = parts[i];
13002
+ const isFile = i === parts.length - 1;
13003
+ const existingChild = current.children?.find((c) => c.name === part);
13004
+ if (existingChild) {
13005
+ current = existingChild;
13006
+ } else {
13007
+ const nodePath = "/" + parts.slice(0, i + 1).join("/");
13008
+ const node = isFile ? {
13009
+ name: part,
13010
+ path: nodePath,
13011
+ key: file.key,
13012
+ type: "file",
13013
+ size: file.size,
13014
+ lastModified: file.lastModified
13015
+ } : { name: part, path: nodePath, key: "", type: "folder", children: [] };
13016
+ current.children = current.children ?? [];
13017
+ current.children.push(node);
13018
+ current = node;
13019
+ }
12138
13020
  }
12139
- ]
12140
- };
12141
- const jsonString = JSON.stringify(responseData);
12142
- const arrayBuffer = new TextEncoder().encode(jsonString).buffer;
12143
- return arrayBuffer;
12144
- };
12145
-
12146
- // src/mcp/index.ts
12147
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12148
- import { randomUUID as randomUUID5 } from "crypto";
12149
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
12150
- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
12151
- import "express";
12152
- import "@opentelemetry/api";
12153
- import CryptoJS7 from "crypto-js";
12154
- import { z as z12 } from "zod";
12155
- var SESSION_ID_HEADER = "mcp-session-id";
12156
- var ExuluMCP = class {
13021
+ }
13022
+ return root;
13023
+ }
13024
+ app.post("/skills/:skillId/init", async (req, res) => {
13025
+ const authResult = await requestValidators.authenticate(req);
13026
+ if (!authResult.user?.id) {
13027
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
13028
+ return;
13029
+ }
13030
+ const { skillId } = req.params;
13031
+ const { name = "Skill", description = "" } = req.body;
13032
+ const skillMdContent = [
13033
+ `# ${name}`,
13034
+ "",
13035
+ description || "Describe what this skill does and when to use it.",
13036
+ "",
13037
+ "## Overview",
13038
+ "",
13039
+ "...",
13040
+ "",
13041
+ "## Usage",
13042
+ "",
13043
+ "..."
13044
+ ].join("\n");
13045
+ const s3Key = `skills/${skillId}/v1/SKILL.md`;
13046
+ try {
13047
+ await uploadFile(Buffer.from(skillMdContent, "utf-8"), s3Key, config, { contentType: "text/markdown" }, void 0, void 0, true);
13048
+ } catch (err) {
13049
+ console.error("[SKILLS] Failed to create SKILL.md in S3", err);
13050
+ res.status(500).json({ detail: "Failed to initialise skill folder in S3." });
13051
+ return;
13052
+ }
13053
+ const { db: db2 } = await postgresClient();
13054
+ await db2("skills").where({ id: skillId }).update({
13055
+ s3folder: `skills/${skillId}`,
13056
+ current_version: 1,
13057
+ history: JSON.stringify([
13058
+ { version: 1, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: "Initial" }
13059
+ ])
13060
+ });
13061
+ res.json({ version: 1, skillMdKey: s3Key });
13062
+ });
13063
+ app.get("/skills/:skillId/files", async (req, res) => {
13064
+ const authResult = await requestValidators.authenticate(req);
13065
+ if (!authResult.user?.id) {
13066
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
13067
+ return;
13068
+ }
13069
+ const { skillId } = req.params;
13070
+ const { db: db2 } = await postgresClient();
13071
+ const skill = await db2("skills").where({ id: skillId }).first();
13072
+ if (!skill) {
13073
+ res.status(404).json({ detail: "Skill not found." });
13074
+ return;
13075
+ }
13076
+ const version = req.query.version ? Number(req.query.version) : skill.current_version ?? 1;
13077
+ const prefix = `skills/${skillId}/v${version}/`;
13078
+ const files = await listS3ObjectsByPrefix(prefix, config);
13079
+ const tree = buildFileTree(files, config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/` + prefix : prefix);
13080
+ res.json({ version, tree, fileCount: files.length });
13081
+ });
13082
+ app.post("/skills/:skillId/sign", async (req, res) => {
13083
+ const authResult = await requestValidators.authenticate(req);
13084
+ if (!authResult.user?.id) {
13085
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
13086
+ return;
13087
+ }
13088
+ const { skillId } = req.params;
13089
+ const { filePath, contentType = "application/octet-stream" } = req.body;
13090
+ if (!filePath || typeof filePath !== "string") {
13091
+ res.status(400).json({ detail: "Missing filePath in request body." });
13092
+ return;
13093
+ }
13094
+ const { db: db2 } = await postgresClient();
13095
+ const skill = await db2("skills").where({ id: skillId }).first();
13096
+ if (!skill) {
13097
+ res.status(404).json({ detail: "Skill not found." });
13098
+ return;
13099
+ }
13100
+ const version = skill.current_version ?? 1;
13101
+ const safePath = filePath.replace(/^\/+/, "").replace(/\.\.\//g, "");
13102
+ const s3Key = `skills/${skillId}/v${version}/${safePath}`;
13103
+ const fullKey = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/${s3Key}` : s3Key;
13104
+ const url = await getS3SignedUploadUrl(fullKey, contentType, config);
13105
+ res.json({ key: s3Key, url, method: "PUT" });
13106
+ });
13107
+ app.get("/skills/:skillId/file", async (req, res) => {
13108
+ const authResult = await requestValidators.authenticate(req);
13109
+ if (!authResult.user?.id) {
13110
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
13111
+ return;
13112
+ }
13113
+ const { skillId } = req.params;
13114
+ const { key } = req.query;
13115
+ if (!key || typeof key !== "string") {
13116
+ res.status(400).json({ detail: "Missing key query parameter." });
13117
+ return;
13118
+ }
13119
+ if (!key.startsWith(`skills/${skillId}/`)) {
13120
+ res.status(403).json({ detail: "Key does not belong to this skill." });
13121
+ return;
13122
+ }
13123
+ const { db: db2 } = await postgresClient();
13124
+ const skill = await db2("skills").where({ id: skillId }).first();
13125
+ if (!skill) {
13126
+ res.status(404).json({ detail: "Skill not found." });
13127
+ return;
13128
+ }
13129
+ const fullKey = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/${key}` : key;
13130
+ const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".txt", ".py", ".js", ".ts", ".json", ".yaml", ".yml", ".sh", ".env", ".toml", ".xml", ".html", ".css"]);
13131
+ const ext = key.slice(key.lastIndexOf(".")).toLowerCase();
13132
+ const isText = TEXT_EXTENSIONS.has(ext);
13133
+ const MAX_INLINE_BYTES = 200 * 1024;
13134
+ let content;
13135
+ if (isText) {
13136
+ try {
13137
+ const raw = await getS3ObjectContent(fullKey, config);
13138
+ if (raw.length <= MAX_INLINE_BYTES) {
13139
+ content = raw;
13140
+ }
13141
+ } catch {
13142
+ }
13143
+ }
13144
+ const bucket = config.fileUploads?.s3Bucket ?? "";
13145
+ const url = await getPresignedUrl(bucket, fullKey, config);
13146
+ res.json({ url, content, key });
13147
+ });
13148
+ app.delete("/skills/:skillId/file", async (req, res) => {
13149
+ const authResult = await requestValidators.authenticate(req);
13150
+ if (!authResult.user?.id) {
13151
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
13152
+ return;
13153
+ }
13154
+ const { skillId } = req.params;
13155
+ const { key, prefix } = req.query;
13156
+ if (!key && !prefix) {
13157
+ res.status(400).json({ detail: "Provide either key or prefix query parameter." });
13158
+ return;
13159
+ }
13160
+ const guard = (s) => !s.startsWith(`skills/${skillId}/`);
13161
+ if (key && typeof key === "string") {
13162
+ if (guard(key)) {
13163
+ res.status(403).json({ detail: "Key does not belong to this skill." });
13164
+ return;
13165
+ }
13166
+ }
13167
+ if (prefix && typeof prefix === "string") {
13168
+ if (guard(prefix)) {
13169
+ res.status(403).json({ detail: "Prefix does not belong to this skill." });
13170
+ return;
13171
+ }
13172
+ }
13173
+ const { db: db2 } = await postgresClient();
13174
+ const skill = await db2("skills").where({ id: skillId }).first();
13175
+ if (!skill) {
13176
+ res.status(404).json({ detail: "Skill not found." });
13177
+ return;
13178
+ }
13179
+ const s3Prefix = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/` : "";
13180
+ if (key && typeof key === "string") {
13181
+ const fullKey = s3Prefix + key;
13182
+ await deleteS3Object(fullKey, config);
13183
+ res.json({ deleted: 1 });
13184
+ return;
13185
+ }
13186
+ if (prefix && typeof prefix === "string") {
13187
+ const files = await listS3ObjectsByPrefix(prefix, config);
13188
+ await Promise.all(files.map((f) => deleteS3Object(f.key, config)));
13189
+ res.json({ deleted: files.length });
13190
+ return;
13191
+ }
13192
+ });
13193
+ app.post("/skills/:skillId/version", async (req, res) => {
13194
+ const authResult = await requestValidators.authenticate(req);
13195
+ if (!authResult.user?.id) {
13196
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
13197
+ return;
13198
+ }
13199
+ const { skillId } = req.params;
13200
+ const { label } = req.body;
13201
+ const { db: db2 } = await postgresClient();
13202
+ const skill = await db2("skills").where({ id: skillId }).first();
13203
+ if (!skill) {
13204
+ res.status(404).json({ detail: "Skill not found." });
13205
+ return;
13206
+ }
13207
+ const currentVersion = skill.current_version ?? 1;
13208
+ const newVersion = currentVersion + 1;
13209
+ const currentPrefix = `skills/${skillId}/v${currentVersion}/`;
13210
+ const newPrefix = `skills/${skillId}/v${newVersion}/`;
13211
+ const files = await listS3ObjectsByPrefix(currentPrefix, config);
13212
+ if (files.length === 0) {
13213
+ res.status(400).json({ detail: "No files found in current version to snapshot." });
13214
+ return;
13215
+ }
13216
+ const s3GeneralPrefix = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/` : "";
13217
+ for (const file of files) {
13218
+ const destKey = file.key.replace(s3GeneralPrefix + currentPrefix, s3GeneralPrefix + newPrefix);
13219
+ await copyS3Object(file.key, destKey, config);
13220
+ }
13221
+ const existingHistory = Array.isArray(skill.history) ? skill.history : [];
13222
+ const newHistory = [
13223
+ ...existingHistory,
13224
+ { version: newVersion, created_at: (/* @__PURE__ */ new Date()).toISOString(), label: label ?? `v${newVersion}` }
13225
+ ];
13226
+ await db2("skills").where({ id: skillId }).update({
13227
+ current_version: newVersion,
13228
+ history: JSON.stringify(newHistory)
13229
+ });
13230
+ res.json({ newVersion, fileCount: files.length });
13231
+ });
13232
+ app.post("/skills/:skillId/rename", async (req, res) => {
13233
+ const authResult = await requestValidators.authenticate(req);
13234
+ if (!authResult.user?.id) {
13235
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
13236
+ return;
13237
+ }
13238
+ const { skillId } = req.params;
13239
+ const { sourceKey, destPath } = req.body;
13240
+ if (!sourceKey || !destPath) {
13241
+ res.status(400).json({ detail: "sourceKey and destPath are required." });
13242
+ return;
13243
+ }
13244
+ if (!sourceKey.startsWith(`skills/${skillId}/`)) {
13245
+ res.status(403).json({ detail: "sourceKey does not belong to this skill." });
13246
+ return;
13247
+ }
13248
+ const { db: db2 } = await postgresClient();
13249
+ const skill = await db2("skills").where({ id: skillId }).first();
13250
+ if (!skill) {
13251
+ res.status(404).json({ detail: "Skill not found." });
13252
+ return;
13253
+ }
13254
+ const version = skill.current_version ?? 1;
13255
+ const safeDest = destPath.replace(/^\/+/, "").replace(/\.\.\//g, "");
13256
+ const destKey = `skills/${skillId}/v${version}/${safeDest}`;
13257
+ const s3Prefix = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/` : "";
13258
+ const fullSourceKey = s3Prefix + sourceKey;
13259
+ const fullDestKey = s3Prefix + destKey;
13260
+ await copyS3Object(fullSourceKey, fullDestKey, config);
13261
+ await deleteS3Object(fullSourceKey, config);
13262
+ res.json({ newKey: destKey });
13263
+ });
13264
+ app.get("/skills/:skillId/diff", async (req, res) => {
13265
+ const authResult = await requestValidators.authenticate(req);
13266
+ if (!authResult.user?.id) {
13267
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
13268
+ return;
13269
+ }
13270
+ const { skillId } = req.params;
13271
+ const fromVersion = Number(req.query.fromVersion);
13272
+ const toVersion = Number(req.query.toVersion);
13273
+ if (!fromVersion || !toVersion) {
13274
+ res.status(400).json({ detail: "fromVersion and toVersion query params are required." });
13275
+ return;
13276
+ }
13277
+ const { db: db2 } = await postgresClient();
13278
+ const skill = await db2("skills").where({ id: skillId }).first();
13279
+ if (!skill) {
13280
+ res.status(404).json({ detail: "Skill not found." });
13281
+ return;
13282
+ }
13283
+ const s3Prefix = config.fileUploads?.s3prefix ? `${config.fileUploads.s3prefix.replace(/\/$/, "")}/` : "";
13284
+ const fromPrefix = `skills/${skillId}/v${fromVersion}/`;
13285
+ const toPrefix = `skills/${skillId}/v${toVersion}/`;
13286
+ const [fromFiles, toFiles] = await Promise.all([
13287
+ listS3ObjectsByPrefix(fromPrefix, config),
13288
+ listS3ObjectsByPrefix(toPrefix, config)
13289
+ ]);
13290
+ const relativise = (files, prefix) => {
13291
+ const full = s3Prefix + prefix;
13292
+ return new Map(files.map((f) => [f.key.replace(full, ""), f]));
13293
+ };
13294
+ const fromMap = relativise(fromFiles, fromPrefix);
13295
+ const toMap = relativise(toFiles, toPrefix);
13296
+ const allPaths = /* @__PURE__ */ new Set([...fromMap.keys(), ...toMap.keys()]);
13297
+ const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".txt", ".py", ".js", ".ts", ".json", ".yaml", ".yml", ".sh", ".toml"]);
13298
+ const MAX_DIFF_BYTES = 500 * 1024;
13299
+ const fileDiffs = await Promise.all(
13300
+ [...allPaths].map(async (path3) => {
13301
+ const inFrom = fromMap.has(path3);
13302
+ const inTo = toMap.has(path3);
13303
+ const status = !inFrom ? "added" : !inTo ? "removed" : "modified";
13304
+ if (status === "modified") {
13305
+ const ext = path3.slice(path3.lastIndexOf(".")).toLowerCase();
13306
+ if (!TEXT_EXTENSIONS.has(ext)) {
13307
+ return { path: path3, status };
13308
+ }
13309
+ try {
13310
+ const [fromContent, toContent] = await Promise.all([
13311
+ getS3ObjectContent(s3Prefix + fromPrefix + path3, config),
13312
+ getS3ObjectContent(s3Prefix + toPrefix + path3, config)
13313
+ ]);
13314
+ if (fromContent === toContent) {
13315
+ return { path: path3, status: "unchanged" };
13316
+ }
13317
+ if (fromContent.length + toContent.length > MAX_DIFF_BYTES) {
13318
+ return { path: path3, status };
13319
+ }
13320
+ const fromLines = fromContent.split("\n");
13321
+ const toLines = toContent.split("\n");
13322
+ const diff = buildUnifiedDiff(fromLines, toLines, `v${fromVersion}/${path3}`, `v${toVersion}/${path3}`);
13323
+ return { path: path3, status, diff };
13324
+ } catch {
13325
+ return { path: path3, status };
13326
+ }
13327
+ }
13328
+ return { path: path3, status };
13329
+ })
13330
+ );
13331
+ res.json({
13332
+ fromVersion,
13333
+ toVersion,
13334
+ files: fileDiffs.filter((f) => f.status !== "unchanged")
13335
+ });
13336
+ });
13337
+ app.use(express2.static("public"));
13338
+ await registerOpenAIGatewayRoutes(app, providers, tools, contexts, config, rerankers);
13339
+ return app;
13340
+ };
13341
+ function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
13342
+ function lcs(a, b) {
13343
+ const m = a.length;
13344
+ const n = b.length;
13345
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
13346
+ for (let i = 1; i <= m; i++) {
13347
+ for (let j = 1; j <= n; j++) {
13348
+ dp[i][j] = a[i - 1] === b[j - 1] ? (dp[i - 1][j - 1] ?? 0) + 1 : Math.max(dp[i - 1][j] ?? 0, dp[i][j - 1] ?? 0);
13349
+ }
13350
+ }
13351
+ return dp;
13352
+ }
13353
+ function diff(a, b) {
13354
+ const table = lcs(a, b);
13355
+ const result = [];
13356
+ let i = a.length;
13357
+ let j = b.length;
13358
+ while (i > 0 || j > 0) {
13359
+ if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
13360
+ result.unshift({ op: "=", line: a[i - 1] });
13361
+ i--;
13362
+ j--;
13363
+ } else if (j > 0 && (i === 0 || (table[i][j - 1] ?? 0) >= (table[i - 1][j] ?? 0))) {
13364
+ result.unshift({ op: "+", line: b[j - 1] });
13365
+ j--;
13366
+ } else {
13367
+ result.unshift({ op: "-", line: a[i - 1] });
13368
+ i--;
13369
+ }
13370
+ }
13371
+ return result;
13372
+ }
13373
+ const CONTEXT = 3;
13374
+ const hunks = diff(fromLines, toLines);
13375
+ const lines = [`--- ${fromLabel}`, `+++ ${toLabel}`];
13376
+ let hunkStart = -1;
13377
+ for (let idx = 0; idx < hunks.length; idx++) {
13378
+ const h = hunks[idx];
13379
+ if (h.op !== "=") {
13380
+ if (hunkStart < 0) {
13381
+ hunkStart = Math.max(0, idx - CONTEXT);
13382
+ }
13383
+ } else if (hunkStart >= 0 && idx - hunkStart > CONTEXT * 2) {
13384
+ const slice = hunks.slice(hunkStart, Math.min(idx, hunkStart + (idx - hunkStart)));
13385
+ lines.push(`@@ -${hunkStart + 1} +${hunkStart + 1} @@`);
13386
+ for (const s of slice) {
13387
+ lines.push((s.op === "=" ? " " : s.op) + s.line);
13388
+ }
13389
+ hunkStart = -1;
13390
+ }
13391
+ }
13392
+ if (hunkStart >= 0) {
13393
+ const slice = hunks.slice(hunkStart, Math.min(hunks.length, hunkStart + hunks.length));
13394
+ lines.push(`@@ -${hunkStart + 1} +${hunkStart + 1} @@`);
13395
+ for (const s of slice) {
13396
+ lines.push((s.op === "=" ? " " : s.op) + s.line);
13397
+ }
13398
+ }
13399
+ return lines.join("\n");
13400
+ }
13401
+ var createCustomAnthropicStreamingMessage = (message) => {
13402
+ const responseData = {
13403
+ type: "message",
13404
+ content: [
13405
+ {
13406
+ type: "text",
13407
+ text: message
13408
+ }
13409
+ ]
13410
+ };
13411
+ const jsonString = JSON.stringify(responseData);
13412
+ const arrayBuffer = new TextEncoder().encode(jsonString).buffer;
13413
+ return arrayBuffer;
13414
+ };
13415
+
13416
+ // src/mcp/index.ts
13417
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13418
+ import { randomUUID as randomUUID6 } from "crypto";
13419
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
13420
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
13421
+ import "express";
13422
+ import "@opentelemetry/api";
13423
+ import CryptoJS8 from "crypto-js";
13424
+ import { z as z11 } from "zod";
13425
+ var SESSION_ID_HEADER = "mcp-session-id";
13426
+ var ExuluMCP = class {
12157
13427
  server = {};
12158
13428
  transports = {};
12159
13429
  constructor() {
@@ -12216,34 +13486,34 @@ var ExuluMCP = class {
12216
13486
  );
12217
13487
  }
12218
13488
  if (variable.encrypted) {
12219
- const bytes = CryptoJS7.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
12220
- providerapikey = bytes.toString(CryptoJS7.enc.Utf8);
13489
+ const bytes = CryptoJS8.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
13490
+ providerapikey = bytes.toString(CryptoJS8.enc.Utf8);
12221
13491
  }
12222
13492
  }
12223
13493
  console.log(
12224
13494
  "[EXULU] Enabled tools",
12225
13495
  enabledTools?.map((x) => x.name + " (" + x.id + ")")
12226
13496
  );
12227
- for (const tool6 of enabledTools || []) {
12228
- if (server.tools[tool6.id]) {
13497
+ for (const tool5 of enabledTools || []) {
13498
+ if (server.tools[tool5.id]) {
12229
13499
  continue;
12230
13500
  }
12231
13501
  server.mcp.registerTool(
12232
- sanitizeToolName(tool6.name + "_agent_" + tool6.id),
13502
+ sanitizeToolName(tool5.name + "_agent_" + tool5.id),
12233
13503
  {
12234
- title: tool6.name + " agent",
12235
- description: tool6.description,
13504
+ title: tool5.name + " agent",
13505
+ description: tool5.description,
12236
13506
  inputSchema: {
12237
- inputs: tool6.inputSchema || z12.object({})
13507
+ inputs: tool5.inputSchema || z11.object({})
12238
13508
  }
12239
13509
  },
12240
13510
  async ({ inputs }, args) => {
12241
- console.log("[EXULU] MCP tool name", tool6.name);
13511
+ console.log("[EXULU] MCP tool name", tool5.name);
12242
13512
  console.log("[EXULU] MCP tool inputs", inputs);
12243
13513
  console.log("[EXULU] MCP tool args", args);
12244
13514
  const configValues = agent.tools;
12245
13515
  const tools = await convertExuluToolsToAiSdkTools(
12246
- [tool6],
13516
+ [tool5],
12247
13517
  [],
12248
13518
  allTools,
12249
13519
  configValues,
@@ -12256,13 +13526,13 @@ var ExuluMCP = class {
12256
13526
  void 0,
12257
13527
  void 0
12258
13528
  );
12259
- const convertedTool = tools[sanitizeToolName(tool6.name)];
13529
+ const convertedTool = tools[sanitizeToolName(tool5.name)];
12260
13530
  if (!convertedTool?.execute) {
12261
13531
  console.error("[EXULU] Tool not found in converted tools array.", tools);
12262
13532
  throw new Error("Tool not found in converted tools array.");
12263
13533
  }
12264
13534
  const iterator = await convertedTool.execute(inputs, {
12265
- toolCallId: tool6.id + "_" + randomUUID5(),
13535
+ toolCallId: tool5.id + "_" + randomUUID6(),
12266
13536
  messages: []
12267
13537
  });
12268
13538
  let result;
@@ -12276,7 +13546,7 @@ var ExuluMCP = class {
12276
13546
  };
12277
13547
  }
12278
13548
  );
12279
- server.tools[tool6.id] = tool6.name;
13549
+ server.tools[tool5.id] = tool5.name;
12280
13550
  }
12281
13551
  const getListOfPromptTemplatesName = "getListOfPromptTemplates";
12282
13552
  if (!server.tools[getListOfPromptTemplatesName]) {
@@ -12286,7 +13556,7 @@ var ExuluMCP = class {
12286
13556
  title: "Get List of Prompt Templates",
12287
13557
  description: "Retrieves a list of prompt templates available for this agent. Returns the name, description, and ID of each template.",
12288
13558
  inputSchema: {
12289
- inputs: z12.object({})
13559
+ inputs: z11.object({})
12290
13560
  }
12291
13561
  },
12292
13562
  async ({ inputs }, args) => {
@@ -12332,8 +13602,8 @@ var ExuluMCP = class {
12332
13602
  title: "Get Prompt Template Details",
12333
13603
  description: "Retrieves the full details of a specific prompt template by ID, including the actual template content with variables.",
12334
13604
  inputSchema: {
12335
- inputs: z12.object({
12336
- id: z12.string().describe("The ID of the prompt template to retrieve")
13605
+ inputs: z11.object({
13606
+ id: z11.string().describe("The ID of the prompt template to retrieve")
12337
13607
  })
12338
13608
  }
12339
13609
  },
@@ -12396,20 +13666,20 @@ var ExuluMCP = class {
12396
13666
  return server.mcp;
12397
13667
  };
12398
13668
  create = async ({
12399
- express: express3,
13669
+ express: express4,
12400
13670
  allTools,
12401
13671
  allProviders,
12402
13672
  allContexts,
12403
13673
  allRerankers,
12404
13674
  config
12405
13675
  }) => {
12406
- if (!express3) {
13676
+ if (!express4) {
12407
13677
  throw new Error("Express not initialized.");
12408
13678
  }
12409
13679
  if (!this.server) {
12410
13680
  throw new Error("MCP server not initialized.");
12411
13681
  }
12412
- express3.post("/mcp/:agent", async (req, res) => {
13682
+ express4.post("/mcp/:agent", async (req, res) => {
12413
13683
  console.log("[EXULU] MCP request received.", req.params.agent);
12414
13684
  if (!req.params.agent) {
12415
13685
  res.status(400).json({
@@ -12456,7 +13726,7 @@ var ExuluMCP = class {
12456
13726
  transport = this.transports[sessionId];
12457
13727
  } else if (!sessionId && isInitializeRequest(req.body)) {
12458
13728
  transport = new StreamableHTTPServerTransport({
12459
- sessionIdGenerator: () => randomUUID5(),
13729
+ sessionIdGenerator: () => randomUUID6(),
12460
13730
  onsessioninitialized: (sessionId2) => {
12461
13731
  this.transports[sessionId2] = transport;
12462
13732
  }
@@ -12490,15 +13760,15 @@ var ExuluMCP = class {
12490
13760
  const transport = this.transports[sessionId];
12491
13761
  await transport.handleRequest(req, res);
12492
13762
  };
12493
- express3.get("/mcp/:agent", handleSessionRequest);
12494
- express3.delete("/mcp/:agent", handleSessionRequest);
13763
+ express4.get("/mcp/:agent", handleSessionRequest);
13764
+ express4.delete("/mcp/:agent", handleSessionRequest);
12495
13765
  console.log("[EXULU] MCP server created.");
12496
- return express3;
13766
+ return express4;
12497
13767
  };
12498
13768
  };
12499
13769
 
12500
13770
  // src/exulu/app/index.ts
12501
- import express2 from "express";
13771
+ import express3 from "express";
12502
13772
 
12503
13773
  // src/templates/providers/anthropic/claude.ts
12504
13774
  import { createAnthropic } from "@ai-sdk/anthropic";
@@ -13241,7 +14511,7 @@ var ExuluEval = class {
13241
14511
  };
13242
14512
 
13243
14513
  // src/templates/evals/index.ts
13244
- import { z as z13 } from "zod";
14514
+ import { z as z12 } from "zod";
13245
14515
  var llmAsJudgeEval = () => {
13246
14516
  if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
13247
14517
  return new ExuluEval({
@@ -13286,8 +14556,8 @@ var llmAsJudgeEval = () => {
13286
14556
  contexts: [],
13287
14557
  rerankers: [],
13288
14558
  prompt,
13289
- outputSchema: z13.object({
13290
- score: z13.number().min(0).max(100).describe("The score between 0 and 100.")
14559
+ outputSchema: z12.object({
14560
+ score: z12.number().min(0).max(100).describe("The score between 0 and 100.")
13291
14561
  }),
13292
14562
  providerapikey
13293
14563
  });
@@ -13515,12 +14785,12 @@ Usage:
13515
14785
  - If no todos exist yet, an empty list will be returned`;
13516
14786
 
13517
14787
  // src/templates/tools/todo/todo.ts
13518
- import z14 from "zod";
13519
- var TodoSchema = z14.object({
13520
- content: z14.string().describe("Brief description of the task"),
13521
- status: z14.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
13522
- priority: z14.string().describe("Priority level of the task: high, medium, low"),
13523
- id: z14.string().describe("Unique identifier for the todo item")
14788
+ import z13 from "zod";
14789
+ var TodoSchema = z13.object({
14790
+ content: z13.string().describe("Brief description of the task"),
14791
+ status: z13.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
14792
+ priority: z13.string().describe("Priority level of the task: high, medium, low"),
14793
+ id: z13.string().describe("Unique identifier for the todo item")
13524
14794
  });
13525
14795
  var TodoWriteTool = new ExuluTool({
13526
14796
  id: "todo_write",
@@ -13536,8 +14806,8 @@ var TodoWriteTool = new ExuluTool({
13536
14806
  default: todowrite_default
13537
14807
  }
13538
14808
  ],
13539
- inputSchema: z14.object({
13540
- todos: z14.array(TodoSchema).describe("The updated todo list")
14809
+ inputSchema: z13.object({
14810
+ todos: z13.array(TodoSchema).describe("The updated todo list")
13541
14811
  }),
13542
14812
  execute: async (inputs) => {
13543
14813
  const { sessionID, todos, user } = inputs;
@@ -13572,7 +14842,7 @@ var TodoReadTool = new ExuluTool({
13572
14842
  id: "todo_read",
13573
14843
  name: "Todo Read",
13574
14844
  description: "Use this tool to read your todo list",
13575
- inputSchema: z14.object({}),
14845
+ inputSchema: z13.object({}),
13576
14846
  type: "function",
13577
14847
  category: "todo",
13578
14848
  config: [
@@ -13698,18 +14968,18 @@ After asking a question, use the Question Read tool to check if the user has ans
13698
14968
  var questionread_default = 'Use this tool to read questions you\'ve asked and check if they\'ve been answered by the user. This tool helps you track the status of questions and retrieve the user\'s selected answers.\n\n## When to Use This Tool\n\nUse this tool proactively in these situations:\n- After asking a question to check if the user has responded\n- To retrieve the user\'s answer before proceeding with implementation\n- To review all questions and answers in the current session\n- When you need to reference a previous answer\n\n## How It Works\n\n- This tool takes no parameters (leave the input blank or empty)\n- Returns an array of all questions in the session\n- Each question includes:\n - `id`: Unique identifier for the question\n - `question`: The question text\n - `answerOptions`: Array of answer options with their IDs and text\n - `status`: Either "pending" (not answered) or "answered"\n - `selectedAnswerId`: The ID of the chosen answer (only present if answered)\n\n## Usage Pattern\n\nTypically you\'ll:\n1. Use Question Ask to pose a question\n2. Wait for the user to respond\n3. Use Question Read to check the answer\n4. Find the selected answer by matching the `selectedAnswerId` with an option in `answerOptions`\n5. Proceed with implementation based on the user\'s choice\n\n## Example Response\n\n```json\n[\n {\n "id": "question123",\n "question": "Which authentication method would you like to implement?",\n "answerOptions": [\n { "id": "ans1", "text": "JWT tokens" },\n { "id": "ans2", "text": "OAuth 2.0" },\n { "id": "ans3", "text": "Session-based auth" },\n { "id": "ans4", "text": "None of the above..." }\n ],\n "status": "answered",\n "selectedAnswerId": "ans1"\n }\n]\n```\n\nIn this example, the user selected "JWT tokens" (id: ans1).\n\n## Important Notes\n\n- If no questions exist in the session, an empty array will be returned\n- Questions remain in the session even after being answered for reference\n- Use the `selectedAnswerId` to find which answer option the user chose by matching it against the `id` field in `answerOptions`\n';
13699
14969
 
13700
14970
  // src/templates/tools/question/question.ts
13701
- import z15 from "zod";
13702
- import { randomUUID as randomUUID6 } from "crypto";
13703
- var AnswerOptionSchema = z15.object({
13704
- id: z15.string().describe("Unique identifier for the answer option"),
13705
- text: z15.string().describe("The text of the answer option")
14971
+ import z14 from "zod";
14972
+ import { randomUUID as randomUUID7 } from "crypto";
14973
+ var AnswerOptionSchema = z14.object({
14974
+ id: z14.string().describe("Unique identifier for the answer option"),
14975
+ text: z14.string().describe("The text of the answer option")
13706
14976
  });
13707
- var _QuestionSchema = z15.object({
13708
- id: z15.string().describe("Unique identifier for the question"),
13709
- question: z15.string().describe("The question to ask the user"),
13710
- answerOptions: z15.array(AnswerOptionSchema).describe("Array of possible answer options"),
13711
- selectedAnswerId: z15.string().optional().describe("The ID of the answer option selected by the user"),
13712
- status: z15.enum(["pending", "answered"]).describe("Status of the question: pending or answered")
14977
+ var _QuestionSchema = z14.object({
14978
+ id: z14.string().describe("Unique identifier for the question"),
14979
+ question: z14.string().describe("The question to ask the user"),
14980
+ answerOptions: z14.array(AnswerOptionSchema).describe("Array of possible answer options"),
14981
+ selectedAnswerId: z14.string().optional().describe("The ID of the answer option selected by the user"),
14982
+ status: z14.enum(["pending", "answered"]).describe("Status of the question: pending or answered")
13713
14983
  });
13714
14984
  var QuestionAskTool = new ExuluTool({
13715
14985
  id: "question_ask",
@@ -13717,6 +14987,7 @@ var QuestionAskTool = new ExuluTool({
13717
14987
  description: "Use this tool to ask a question to the user with multiple choice answers",
13718
14988
  type: "function",
13719
14989
  category: "question",
14990
+ needsApproval: false,
13720
14991
  config: [
13721
14992
  {
13722
14993
  name: "description",
@@ -13725,9 +14996,9 @@ var QuestionAskTool = new ExuluTool({
13725
14996
  default: questionask_default
13726
14997
  }
13727
14998
  ],
13728
- inputSchema: z15.object({
13729
- question: z15.string().describe("The question to ask the user"),
13730
- answerOptions: z15.array(z15.string()).describe("Array of possible answer options (strings)")
14999
+ inputSchema: z14.object({
15000
+ question: z14.string().describe("The question to ask the user"),
15001
+ answerOptions: z14.array(z14.string()).describe("Array of possible answer options (strings)")
13731
15002
  }),
13732
15003
  execute: async (inputs) => {
13733
15004
  const { sessionID, question, answerOptions, user } = inputs;
@@ -13752,15 +15023,15 @@ var QuestionAskTool = new ExuluTool({
13752
15023
  throw new Error("You don't have access to this session " + session.id + ".");
13753
15024
  }
13754
15025
  const answerOptionsWithIds = answerOptions.map((text) => ({
13755
- id: randomUUID6(),
15026
+ id: randomUUID7(),
13756
15027
  text
13757
15028
  }));
13758
15029
  answerOptionsWithIds.push({
13759
- id: randomUUID6(),
15030
+ id: randomUUID7(),
13760
15031
  text: "None of the above..."
13761
15032
  });
13762
15033
  const newQuestion = {
13763
- id: randomUUID6(),
15034
+ id: randomUUID7(),
13764
15035
  question,
13765
15036
  answerOptions: answerOptionsWithIds,
13766
15037
  status: "pending"
@@ -13786,8 +15057,9 @@ var QuestionAskTool = new ExuluTool({
13786
15057
  var QuestionReadTool = new ExuluTool({
13787
15058
  id: "question_read",
13788
15059
  name: "Question Read",
15060
+ needsApproval: false,
13789
15061
  description: "Use this tool to read questions and their answers",
13790
- inputSchema: z15.object({}),
15062
+ inputSchema: z14.object({}),
13791
15063
  type: "function",
13792
15064
  category: "question",
13793
15065
  config: [
@@ -13827,15 +15099,15 @@ async function getQuestions(sessionID) {
13827
15099
  var questionTools = [QuestionAskTool, QuestionReadTool];
13828
15100
 
13829
15101
  // src/templates/tools/perplexity.ts
13830
- import z16 from "zod";
15102
+ import z15 from "zod";
13831
15103
  import Perplexity from "@perplexity-ai/perplexity_ai";
13832
15104
  var internetSearchTool = new ExuluTool({
13833
15105
  id: "internet_search",
13834
- name: "Perplexity Live Internet Search",
15106
+ name: "Internet Search",
13835
15107
  description: "Search the internet for information.",
13836
- inputSchema: z16.object({
13837
- query: z16.string().describe("The query to the tool."),
13838
- search_recency_filter: z16.enum(["day", "week", "month", "year"]).optional().describe("The recency filter for the search, can be day, week, month or year.")
15108
+ inputSchema: z15.object({
15109
+ query: z15.string().describe("The query to the tool."),
15110
+ search_recency_filter: z15.enum(["day", "week", "month", "year"]).optional().describe("The recency filter for the search, can be day, week, month or year.")
13839
15111
  }),
13840
15112
  category: "internet_search",
13841
15113
  type: "web_search",
@@ -13913,7 +15185,7 @@ var internetSearchTool = new ExuluTool({
13913
15185
  } catch (error) {
13914
15186
  if (error instanceof Perplexity.RateLimitError && attempt < maxRetries - 1) {
13915
15187
  const delay = Math.pow(2, attempt) * 1e3 + Math.random() * 1e3;
13916
- await new Promise((resolve4) => setTimeout(resolve4, delay));
15188
+ await new Promise((resolve3) => setTimeout(resolve3, delay));
13917
15189
  continue;
13918
15190
  }
13919
15191
  throw error;
@@ -13926,6 +15198,101 @@ var internetSearchTool = new ExuluTool({
13926
15198
  });
13927
15199
  var perplexityTools = [internetSearchTool];
13928
15200
 
15201
+ // src/templates/tools/email.ts
15202
+ import * as nodemailer from "nodemailer";
15203
+ import { z as z16 } from "zod";
15204
+ var transporter = null;
15205
+ function getTransporter(config) {
15206
+ if (!transporter) {
15207
+ transporter = nodemailer.createTransport(config);
15208
+ }
15209
+ return transporter;
15210
+ }
15211
+ async function sendEmail(recipient, subject, html, text, config) {
15212
+ const transport = getTransporter(config);
15213
+ html = html.trim();
15214
+ text = text.trim();
15215
+ await transport.sendMail({
15216
+ from: config.from,
15217
+ to: recipient,
15218
+ subject,
15219
+ text,
15220
+ html
15221
+ });
15222
+ }
15223
+ var emailTool = new ExuluTool({
15224
+ id: "email",
15225
+ name: "Email",
15226
+ description: "Send an email.",
15227
+ inputSchema: z16.object({
15228
+ recipient: z16.string().describe("The recipient of the email."),
15229
+ subject: z16.string().describe("The subject of the email."),
15230
+ html: z16.string().describe("The HTML body of the email."),
15231
+ text: z16.string().describe("The text body of the email.")
15232
+ }),
15233
+ type: "function",
15234
+ config: [{
15235
+ name: "smtp_host",
15236
+ description: "The SMTP host to send the email from.",
15237
+ type: "variable",
15238
+ default: void 0
15239
+ }, {
15240
+ name: "smtp_port",
15241
+ description: "The SMTP port to send the email from.",
15242
+ type: "variable",
15243
+ default: void 0
15244
+ }, {
15245
+ name: "smtp_user",
15246
+ description: "The SMTP user to send the email from.",
15247
+ type: "variable",
15248
+ default: void 0
15249
+ }, {
15250
+ name: "smtp_password",
15251
+ description: "The SMTP password to send the email from.",
15252
+ type: "variable",
15253
+ default: void 0
15254
+ }, {
15255
+ name: "smtp_from",
15256
+ description: "The SMTP from address to send the email from.",
15257
+ type: "variable",
15258
+ default: void 0
15259
+ }, {
15260
+ name: "allowed_recipient_domains",
15261
+ description: "A comma seperated list of allowed recipient domains to send emails to.",
15262
+ type: "string",
15263
+ default: void 0
15264
+ }],
15265
+ execute: async ({ recipient, subject, html, text, toolVariablesConfig }) => {
15266
+ const EMAIL_CONFIG = {
15267
+ host: toolVariablesConfig.smtp_host || process.env.SMTP_HOST || "",
15268
+ port: parseInt(toolVariablesConfig.smtp_port || process.env.SMTP_PORT || "587", 10),
15269
+ secure: toolVariablesConfig.smtp_secure === "true" || process.env.SMTP_SECURE === "true",
15270
+ // true for 465, false for other ports
15271
+ auth: {
15272
+ user: toolVariablesConfig.smtp_user || process.env.SMTP_USER || "",
15273
+ pass: toolVariablesConfig.smtp_password || process.env.SMTP_PASSWORD || ""
15274
+ },
15275
+ from: toolVariablesConfig.smtp_from || process.env.SMTP_FROM || "",
15276
+ // Allow self-signed certificates if SMTP_REJECT_UNAUTHORIZED is set to 'false'
15277
+ tls: {
15278
+ rejectUnauthorized: false
15279
+ }
15280
+ };
15281
+ if (toolVariablesConfig.allowed_recipient_domains) {
15282
+ const allowedRecipientDomains = toolVariablesConfig.allowed_recipient_domains.split(",");
15283
+ if (!allowedRecipientDomains.some((domain) => recipient.endsWith(`@${domain}`))) {
15284
+ return {
15285
+ result: "Recipient domain not allowed to send emails to."
15286
+ };
15287
+ }
15288
+ }
15289
+ await sendEmail(recipient, subject, html, text, EMAIL_CONFIG);
15290
+ return {
15291
+ result: "Email sent successfully to " + recipient + " with subject " + subject + "."
15292
+ };
15293
+ }
15294
+ });
15295
+
13929
15296
  // src/validators/postgres-name.ts
13930
15297
  var isValidPostgresName = (id) => {
13931
15298
  const regex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
@@ -14030,8 +15397,7 @@ var ExuluApp2 = class {
14030
15397
  ...todoTools,
14031
15398
  ...questionTools,
14032
15399
  ...perplexityTools,
14033
- // Add contexts as tools
14034
- ...Object.values(contexts || {}).map((context) => context.tool()).filter(Boolean)
15400
+ emailTool
14035
15401
  // Because agents are stored in the database, we add those as tools
14036
15402
  // at request time, not during ExuluApp initialization. We add them
14037
15403
  // in the grahql tools resolver.
@@ -14048,9 +15414,9 @@ var ExuluApp2 = class {
14048
15414
  id: provider.id ?? "",
14049
15415
  type: "agent"
14050
15416
  })),
14051
- ...this._tools.map((tool6) => ({
14052
- name: tool6.name ?? "",
14053
- id: tool6.id ?? "",
15417
+ ...this._tools.map((tool5) => ({
15418
+ name: tool5.name ?? "",
15419
+ id: tool5.id ?? "",
14054
15420
  type: "tool"
14055
15421
  })),
14056
15422
  ...this._rerankers.map((reranker) => ({
@@ -14096,7 +15462,7 @@ var ExuluApp2 = class {
14096
15462
  express = {
14097
15463
  init: async () => {
14098
15464
  if (!this._expressApp) {
14099
- this._expressApp = express2();
15465
+ this._expressApp = express3();
14100
15466
  await this.server.express.init();
14101
15467
  console.log("[EXULU] Express app initialized.");
14102
15468
  }
@@ -14680,7 +16046,7 @@ var RecursiveChunk = class _RecursiveChunk extends Chunk {
14680
16046
  // ee/tokenizer.ts
14681
16047
  import { Tiktoken } from "tiktoken/lite";
14682
16048
  import { load } from "tiktoken/load";
14683
- import registry from "tiktoken/registry.json" with { type: "json" };
16049
+ import registry2 from "tiktoken/registry.json" with { type: "json" };
14684
16050
  import models from "tiktoken/model_to_encoding.json" with { type: "json" };
14685
16051
  var ExuluTokenizer = class {
14686
16052
  constructor() {
@@ -14692,7 +16058,7 @@ var ExuluTokenizer = class {
14692
16058
  }
14693
16059
  const time = performance.now();
14694
16060
  console.log("[EXULU] Loading tokenizer.", modelName);
14695
- const model = await load(registry[models[modelName]]);
16061
+ const model = await load(registry2[models[modelName]]);
14696
16062
  console.log("[EXULU] Loaded tokenizer.", modelName, performance.now() - time);
14697
16063
  const encoder = new Tiktoken(model.bpe_ranks, model.special_tokens, model.pat_str);
14698
16064
  console.log("[EXULU] Set encoder.");
@@ -15312,7 +16678,7 @@ var RecursiveChunker = class _RecursiveChunker extends BaseChunker {
15312
16678
  };
15313
16679
 
15314
16680
  // src/exulu/embedder.ts
15315
- import CryptoJS8 from "crypto-js";
16681
+ import CryptoJS9 from "crypto-js";
15316
16682
  var ExuluEmbedder = class {
15317
16683
  id;
15318
16684
  name;
@@ -15383,8 +16749,8 @@ var ExuluEmbedder = class {
15383
16749
  );
15384
16750
  }
15385
16751
  try {
15386
- const bytes = CryptoJS8.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
15387
- const decrypted = bytes.toString(CryptoJS8.enc.Utf8);
16752
+ const bytes = CryptoJS9.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
16753
+ const decrypted = bytes.toString(CryptoJS9.enc.Utf8);
15388
16754
  if (!decrypted) {
15389
16755
  throw new Error("Decryption returned empty string - invalid key or corrupted data");
15390
16756
  }
@@ -15872,6 +17238,7 @@ var {
15872
17238
  agentMessagesSchema: agentMessagesSchema3,
15873
17239
  rolesSchema: rolesSchema3,
15874
17240
  usersSchema: usersSchema3,
17241
+ skillsSchema: skillsSchema3,
15875
17242
  statisticsSchema: statisticsSchema3,
15876
17243
  variablesSchema: variablesSchema3,
15877
17244
  workflowTemplatesSchema: workflowTemplatesSchema3,
@@ -15922,6 +17289,7 @@ var up = async function(knex) {
15922
17289
  agentsSchema3(),
15923
17290
  feedbackSchema3(),
15924
17291
  variablesSchema3(),
17292
+ skillsSchema3(),
15925
17293
  workflowTemplatesSchema3()
15926
17294
  ];
15927
17295
  const createTable = async (schema) => {
@@ -16124,7 +17492,7 @@ var create = ({
16124
17492
  };
16125
17493
 
16126
17494
  // src/index.ts
16127
- import CryptoJS9 from "crypto-js";
17495
+ import CryptoJS10 from "crypto-js";
16128
17496
 
16129
17497
  // ee/chunking/markdown.ts
16130
17498
  var extractPageTag = (text) => {
@@ -16808,12 +18176,12 @@ Or manually run the setup script:
16808
18176
  }
16809
18177
 
16810
18178
  // ee/python/documents/processing/doc_processor.ts
16811
- import * as fs5 from "fs";
18179
+ import * as fs4 from "fs";
16812
18180
  import * as path2 from "path";
16813
- import { generateText as generateText5, Output as Output3 } from "ai";
18181
+ import { generateText as generateText6, Output as Output3 } from "ai";
16814
18182
  import { z as z17 } from "zod";
16815
18183
  import pLimit from "p-limit";
16816
- import { randomUUID as randomUUID7 } from "crypto";
18184
+ import { randomUUID as randomUUID8 } from "crypto";
16817
18185
  import * as mammoth from "mammoth";
16818
18186
  import TurndownService from "turndown";
16819
18187
  import WordExtractor from "word-extractor";
@@ -16990,6 +18358,55 @@ async function processWord(file) {
16990
18358
  markdown: content
16991
18359
  };
16992
18360
  }
18361
+ async function processImage(buffer, paths, config, verbose = false) {
18362
+ try {
18363
+ await fs4.promises.mkdir(paths.images, { recursive: true });
18364
+ const imagePath = path2.join(paths.images, "1.png");
18365
+ await fs4.promises.writeFile(imagePath, buffer);
18366
+ console.log(`[EXULU] Image saved to: ${imagePath}`);
18367
+ let json = [{
18368
+ page: 1,
18369
+ content: "",
18370
+ // Empty initially, will be populated by VLM if enabled
18371
+ image: imagePath,
18372
+ headings: []
18373
+ }];
18374
+ if (config?.vlm?.model) {
18375
+ console.log("[EXULU] Extracting content from image using VLM...");
18376
+ json = await validateWithVLM(
18377
+ json,
18378
+ config.vlm.model,
18379
+ verbose,
18380
+ config.vlm.concurrency
18381
+ );
18382
+ await fs4.promises.writeFile(
18383
+ paths.json,
18384
+ JSON.stringify(json, null, 2),
18385
+ "utf-8"
18386
+ );
18387
+ console.log("[EXULU] VLM content extraction complete");
18388
+ const correctedCount = json.filter((p) => p.vlm_corrected_text).length;
18389
+ console.log(`[EXULU] Content extracted: ${correctedCount > 0 ? "Yes" : "No"}`);
18390
+ } else {
18391
+ console.log("[EXULU] No VLM configured, image saved without content extraction");
18392
+ console.log("[EXULU] Note: Enable VLM in config to extract text/content from images");
18393
+ await fs4.promises.writeFile(
18394
+ paths.json,
18395
+ JSON.stringify(json, null, 2),
18396
+ "utf-8"
18397
+ );
18398
+ }
18399
+ const markdown = json.map((p) => p.vlm_corrected_text ?? p.content).join("\n\n\n<!-- END_OF_PAGE -->\n\n\n");
18400
+ await fs4.promises.writeFile(paths.markdown, markdown, "utf-8");
18401
+ return {
18402
+ markdown,
18403
+ json
18404
+ };
18405
+ } catch (error) {
18406
+ console.error("[EXULU] Error processing image:", error);
18407
+ throw error;
18408
+ }
18409
+ }
16993
18410
  function normalizeMarkdownContent(content) {
16994
18411
  const lines = content.split("\n");
16995
18412
  const normalizedLines = [];
@@ -17033,7 +18450,7 @@ function reconstructHeadings(correctedText, headingsHierarchy) {
17033
18450
  return result;
17034
18451
  }
17035
18452
  async function validatePageWithVLM(page, imagePath, model) {
17036
- const imageBuffer = await fs5.promises.readFile(imagePath);
18453
+ const imageBuffer = await fs4.promises.readFile(imagePath);
17037
18454
  const imageBase64 = imageBuffer.toString("base64");
17038
18455
  const mimeType = "image/png";
17039
18456
  const prompt = `You are a document validation assistant. Your task is to analyze a page image and correct the output of an OCR/parsing pipeline. The content may include tables, technical diagrams, schematics, and structured text.
@@ -17111,7 +18528,7 @@ If the page contains a flow-chart, schematic, technical drawing or control board
17111
18528
 
17112
18529
  ### 7. Only populate \`corrected_text\` when \`needs_correction\` is true. If the OCR output is accurate, return \`needs_correction: false\` and \`corrected_content: null\`.
17113
18530
  `;
17114
- const result = await generateText5({
18531
+ const result = await generateText6({
17115
18532
  model,
17116
18533
  output: Output3.object({
17117
18534
  schema: z17.object({
@@ -17146,6 +18563,7 @@ If the page contains a flow-chart, schematic, technical drawing or control board
17146
18563
  current_page_table: parsedOutput.current_page_table || void 0,
17147
18564
  reasoning: parsedOutput.reasoning
17148
18565
  };
18566
+ console.log(`[EXULU] VLM validation result: ${JSON.stringify(validation)}`);
17149
18567
  return validation;
17150
18568
  }
17151
18569
  function reconstructTableHeaders(document, validationResults, verbose = false) {
@@ -17201,26 +18619,29 @@ async function validateWithVLM(document, model, verbose = false, concurrency = 1
17201
18619
  let correctedCount = 0;
17202
18620
  const validationTasks = document.map(
17203
18621
  (page) => limit(async () => {
17204
- await new Promise((resolve4) => setImmediate(resolve4));
18622
+ await new Promise((resolve3) => setImmediate(resolve3));
17205
18623
  const imagePath = page.image;
17206
- if (!page.content) {
17207
- console.warn(`[EXULU] Page ${page.page}: No content found, skipping validation`);
17208
- return;
17209
- }
17210
18624
  if (!imagePath) {
17211
18625
  console.warn(`[EXULU] Page ${page.page}: No image found, skipping validation`);
17212
18626
  return;
17213
18627
  }
17214
- const hasImage = page.content.match(/\.(jpeg|jpg|png|gif|webp)/i);
17215
- const hasTable = (page.content.match(/\|/g)?.length || 0) > 1;
17216
- if (!hasImage && !hasTable) {
18628
+ if (page.content) {
18629
+ const hasImage = page.content.match(/\.(jpeg|jpg|png|gif|webp)/i);
18630
+ const hasTable = (page.content.match(/\|/g)?.length || 0) > 1;
18631
+ if (!hasImage && !hasTable) {
18632
+ if (verbose) {
18633
+ console.log(`[EXULU] Page ${page.page}: No image or table found, SKIPPING VLM validation`);
18634
+ }
18635
+ return;
18636
+ }
18637
+ } else {
17217
18638
  if (verbose) {
17218
- console.log(`[EXULU] Page ${page.page}: No image or table found, SKIPPING VLM validation`);
18639
+ console.log(`[EXULU] Page ${page.page}: Standalone image, proceeding with VLM content extraction`);
17219
18640
  }
17220
- return;
17221
18641
  }
17222
18642
  let validation;
17223
18643
  try {
18644
+ console.log(`[EXULU] Validating page ${page.page} with VLM`);
17224
18645
  validation = await withRetry(async () => {
17225
18646
  return await validatePageWithVLM(page, imagePath, model);
17226
18647
  }, 3);
@@ -17303,6 +18724,13 @@ async function processDocument(filePath, fileType, buffer, tempDir, config, verb
17303
18724
  case "doc":
17304
18725
  result = await processWord(buffer);
17305
18726
  break;
18727
+ case "jpg":
18728
+ case "jpeg":
18729
+ case "png":
18730
+ case "gif":
18731
+ case "webp":
18732
+ result = await processImage(buffer, paths, config, verbose);
18733
+ break;
17306
18734
  // Todo other file types with docx and officeparser
17307
18735
  default:
17308
18736
  throw new Error(`[EXULU] Unsupported file type: ${fileType}`);
@@ -17365,7 +18793,7 @@ ${setupResult.output || ""}`);
17365
18793
  if (!result.success) {
17366
18794
  throw new Error(`Document processing failed: ${result.stderr}`);
17367
18795
  }
17368
- const jsonContent = await fs5.promises.readFile(paths.json, "utf-8");
18796
+ const jsonContent = await fs4.promises.readFile(paths.json, "utf-8");
17369
18797
  json = JSON.parse(jsonContent);
17370
18798
  } else if (config?.processor.name === "officeparser") {
17371
18799
  const text = await parseOfficeAsync2(buffer, {
@@ -17382,7 +18810,7 @@ ${setupResult.output || ""}`);
17382
18810
  if (!MISTRAL_API_KEY) {
17383
18811
  throw new Error('[EXULU] MISTRAL_API_KEY is not set, please set it in the environment variable via process.env or via an Exulu variable named "MISTRAL_API_KEY".');
17384
18812
  }
17385
- await new Promise((resolve4) => setTimeout(resolve4, Math.floor(Math.random() * 4e3) + 1e3));
18813
+ await new Promise((resolve3) => setTimeout(resolve3, Math.floor(Math.random() * 4e3) + 1e3));
17386
18814
  const base64Pdf = buffer.toString("base64");
17387
18815
  const client2 = new Mistral({ apiKey: MISTRAL_API_KEY });
17388
18816
  const ocrResponse = await withRetry(async () => {
@@ -17398,9 +18826,9 @@ ${setupResult.output || ""}`);
17398
18826
  }, 10);
17399
18827
  const parser = new LiteParse();
17400
18828
  const screenshots = await parser.screenshot(paths.source, void 0);
17401
- await fs5.promises.mkdir(paths.images, { recursive: true });
18829
+ await fs4.promises.mkdir(paths.images, { recursive: true });
17402
18830
  for (const screenshot of screenshots) {
17403
- await fs5.promises.writeFile(
18831
+ await fs4.promises.writeFile(
17404
18832
  path2.join(
17405
18833
  paths.images,
17406
18834
  `${screenshot.pageNum}.png`
@@ -17415,15 +18843,15 @@ ${setupResult.output || ""}`);
17415
18843
  image: screenshots.find((s) => s.pageNum === page.index + 1)?.imagePath,
17416
18844
  headings: []
17417
18845
  }));
17418
- fs5.writeFileSync(paths.json, JSON.stringify(json, null, 2));
18846
+ fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
17419
18847
  } else if (config?.processor.name === "liteparse") {
17420
18848
  const parser = new LiteParse();
17421
18849
  const result = await parser.parse(paths.source);
17422
18850
  const screenshots = await parser.screenshot(paths.source, void 0);
17423
18851
  console.log(`[EXULU] Liteparse screenshots: ${JSON.stringify(screenshots)}`);
17424
- await fs5.promises.mkdir(paths.images, { recursive: true });
18852
+ await fs4.promises.mkdir(paths.images, { recursive: true });
17425
18853
  for (const screenshot of screenshots) {
17426
- await fs5.promises.writeFile(path2.join(paths.images, `${screenshot.pageNum}.png`), screenshot.imageBuffer);
18854
+ await fs4.promises.writeFile(path2.join(paths.images, `${screenshot.pageNum}.png`), screenshot.imageBuffer);
17427
18855
  screenshot.imagePath = path2.join(paths.images, `${screenshot.pageNum}.png`);
17428
18856
  }
17429
18857
  json = result.pages.map((page) => ({
@@ -17431,7 +18859,7 @@ ${setupResult.output || ""}`);
17431
18859
  content: page.text,
17432
18860
  image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
17433
18861
  }));
17434
- fs5.writeFileSync(paths.json, JSON.stringify(json, null, 2));
18862
+ fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
17435
18863
  }
17436
18864
  console.log(`[EXULU]
17437
18865
  \u2713 Document processing completed successfully`);
@@ -17462,13 +18890,13 @@ ${setupResult.output || ""}`);
17462
18890
  console.log(`[EXULU] Corrected: ${page.vlm_corrected_text.substring(0, 150)}...`);
17463
18891
  });
17464
18892
  }
17465
- await fs5.promises.writeFile(
18893
+ await fs4.promises.writeFile(
17466
18894
  paths.json,
17467
18895
  JSON.stringify(json, null, 2),
17468
18896
  "utf-8"
17469
18897
  );
17470
18898
  }
17471
- const markdownStream = fs5.createWriteStream(paths.markdown, { encoding: "utf-8" });
18899
+ const markdownStream = fs4.createWriteStream(paths.markdown, { encoding: "utf-8" });
17472
18900
  for (let i = 0; i < json.length; i++) {
17473
18901
  const p = json[i];
17474
18902
  if (!p) continue;
@@ -17478,13 +18906,13 @@ ${setupResult.output || ""}`);
17478
18906
  markdownStream.write("\n\n\n<!-- END_OF_PAGE -->\n\n\n");
17479
18907
  }
17480
18908
  }
17481
- await new Promise((resolve4, reject) => {
17482
- markdownStream.end(() => resolve4());
18909
+ await new Promise((resolve3, reject) => {
18910
+ markdownStream.end(() => resolve3());
17483
18911
  markdownStream.on("error", reject);
17484
18912
  });
17485
18913
  console.log(`[EXULU] Validated output saved to: ${paths.json}`);
17486
18914
  console.log(`[EXULU] Validated markdown saved to: ${paths.markdown}`);
17487
- const markdown = await fs5.promises.readFile(paths.markdown, "utf-8");
18915
+ const markdown = await fs4.promises.readFile(paths.markdown, "utf-8");
17488
18916
  const processedJson = json.map((e) => {
17489
18917
  const finalContent = e.vlm_corrected_text ?? e.content;
17490
18918
  return {
@@ -17511,11 +18939,11 @@ var loadFile = async (file, name, tempDir) => {
17511
18939
  if (!fileType) {
17512
18940
  throw new Error("[EXULU] File name does not include extension, extension is required for document processing.");
17513
18941
  }
17514
- const UUID = randomUUID7();
18942
+ const UUID = randomUUID8();
17515
18943
  let buffer;
17516
18944
  if (Buffer.isBuffer(file)) {
17517
18945
  filePath = path2.join(tempDir, `${UUID}.${fileType}`);
17518
- await fs5.promises.writeFile(filePath, file);
18946
+ await fs4.promises.writeFile(filePath, file);
17519
18947
  buffer = file;
17520
18948
  } else {
17521
18949
  filePath = filePath.trim();
@@ -17523,11 +18951,11 @@ var loadFile = async (file, name, tempDir) => {
17523
18951
  const response = await fetch(filePath);
17524
18952
  const array = await response.arrayBuffer();
17525
18953
  const tempFilePath = path2.join(tempDir, `${UUID}.${fileType}`);
17526
- await fs5.promises.writeFile(tempFilePath, Buffer.from(array));
18954
+ await fs4.promises.writeFile(tempFilePath, Buffer.from(array));
17527
18955
  buffer = Buffer.from(array);
17528
18956
  filePath = tempFilePath;
17529
18957
  } else {
17530
- buffer = await fs5.promises.readFile(file);
18958
+ buffer = await fs4.promises.readFile(file);
17531
18959
  }
17532
18960
  }
17533
18961
  return { filePath, fileType, buffer };
@@ -17541,13 +18969,13 @@ async function documentProcessor({
17541
18969
  if (!license["advanced-document-processing"]) {
17542
18970
  throw new Error("Advanced document processing is an enterprise feature, please add a valid Exulu enterprise license key to use it.");
17543
18971
  }
17544
- const uuid = randomUUID7();
18972
+ const uuid = randomUUID8();
17545
18973
  const tempDir = path2.join(process.cwd(), "temp", uuid);
17546
18974
  const localFilesAndFoldersToDelete = [tempDir];
17547
18975
  console.log(`[EXULU] Temporary directory for processing document ${name}: ${tempDir}`);
17548
- await fs5.promises.mkdir(tempDir, { recursive: true });
18976
+ await fs4.promises.mkdir(tempDir, { recursive: true });
17549
18977
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
17550
- await fs5.promises.writeFile(path2.join(tempDir, "created_at.txt"), timestamp);
18978
+ await fs4.promises.writeFile(path2.join(tempDir, "created_at.txt"), timestamp);
17551
18979
  try {
17552
18980
  const {
17553
18981
  filePath,
@@ -17557,7 +18985,7 @@ async function documentProcessor({
17557
18985
  let supportedTypes = [];
17558
18986
  switch (config?.processor.name) {
17559
18987
  case "docling":
17560
- supportedTypes = ["pdf", "docx", "doc", "txt", "md"];
18988
+ supportedTypes = ["pdf", "docx", "doc", "txt", "md", "jpg", "jpeg", "png", "gif", "webp"];
17561
18989
  break;
17562
18990
  case "officeparser":
17563
18991
  supportedTypes = [];
@@ -17566,7 +18994,7 @@ async function documentProcessor({
17566
18994
  supportedTypes = ["pdf", "doc", "docx", "docm", "odt", "rtf", "ppt", "pptx", "pptm", "odp", "xls", "xlsx", "xlsm", "ods", "csv", "tsv"];
17567
18995
  break;
17568
18996
  case "mistral":
17569
- supportedTypes = ["pdf", "docx", "doc", "txt", "md"];
18997
+ supportedTypes = ["pdf", "docx", "doc", "txt", "md", "jpg", "jpeg", "png", "gif", "webp"];
17570
18998
  break;
17571
18999
  }
17572
19000
  if (!supportedTypes.includes(fileType)) {
@@ -17588,7 +19016,7 @@ async function documentProcessor({
17588
19016
  if (config?.debugging?.deleteTempFiles !== false) {
17589
19017
  for (const file2 of localFilesAndFoldersToDelete) {
17590
19018
  try {
17591
- await fs5.promises.rm(file2, { recursive: true });
19019
+ await fs4.promises.rm(file2, { recursive: true });
17592
19020
  console.log(`[EXULU] Deleted file or folder: ${file2}`);
17593
19021
  } catch (error) {
17594
19022
  console.error(`[EXULU] Error deleting file or folder: ${file2}`, error);
@@ -17599,634 +19027,6 @@ async function documentProcessor({
17599
19027
  }
17600
19028
  }
17601
19029
 
17602
- // ee/agentic-retrieval/v4/index.ts
17603
- import * as os from "os";
17604
- import * as path4 from "path";
17605
- import * as fs7 from "fs/promises";
17606
- import { z as z19 } from "zod";
17607
- import { randomUUID as randomUUID8 } from "crypto";
17608
-
17609
- // ee/agentic-retrieval/v4/tools.ts
17610
- import * as fs6 from "fs/promises";
17611
- import * as path3 from "path";
17612
- import { exec as exec3 } from "child_process";
17613
- import { promisify as promisify3 } from "util";
17614
- import { z as z18 } from "zod";
17615
- import { tool as tool5 } from "ai";
17616
-
17617
- // ee/agentic-retrieval/v4/embed-preprocessor.ts
17618
- async function preprocessEmbedCalls(sql, contexts, user, role) {
17619
- const EMBED_RE = /embed\('((?:[^'\\]|\\.)*)'\s*(?:,\s*'((?:[^'\\]|\\.)*)')?\)/gi;
17620
- const matches = [];
17621
- let m;
17622
- while ((m = EMBED_RE.exec(sql)) !== null) {
17623
- matches.push({
17624
- fullMatch: m[0],
17625
- text: m[1],
17626
- contextId: m[2] || void 0,
17627
- index: m.index
17628
- });
17629
- }
17630
- if (matches.length === 0) return sql;
17631
- const substitutions = await Promise.all(
17632
- matches.map(async ({ text, contextId }) => {
17633
- const context = contextId ? contexts.find((c) => c.id === contextId) : contexts.find((c) => c.embedder != null);
17634
- if (!context?.embedder) {
17635
- throw new Error(
17636
- `No embedder available${contextId ? ` for context "${contextId}"` : ""}. Available contexts with embedders: [${contexts.filter((c) => c.embedder).map((c) => c.id).join(", ")}]`
17637
- );
17638
- }
17639
- const result2 = await context.embedder.generateFromQuery(
17640
- context.id,
17641
- text,
17642
- void 0,
17643
- user?.id,
17644
- role
17645
- );
17646
- const vector = result2?.chunks?.[0]?.vector;
17647
- if (!vector?.length) {
17648
- throw new Error(`Embedder returned no vector for text: "${text}"`);
17649
- }
17650
- return `ARRAY[${vector.join(",")}]::vector`;
17651
- })
17652
- );
17653
- let result = sql;
17654
- for (let i = matches.length - 1; i >= 0; i--) {
17655
- const { fullMatch, index } = matches[i];
17656
- result = result.slice(0, index) + substitutions[i] + result.slice(index + fullMatch.length);
17657
- }
17658
- return result;
17659
- }
17660
-
17661
- // ee/agentic-retrieval/v4/tools.ts
17662
- var execAsync3 = promisify3(exec3);
17663
- var MAX_INLINE_CHARS = 2e4;
17664
- var MAX_GREP_OUTPUT_CHARS = 5e3;
17665
- var WRITE_PATTERN = /^\s*(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|REVOKE|VACUUM|ANALYZE|EXPLAIN\s+ANALYZE)\b/i;
17666
- function assertReadOnly(sql) {
17667
- if (WRITE_PATTERN.test(sql)) {
17668
- throw new Error(
17669
- "Only SELECT queries are allowed. Write operations (INSERT, UPDATE, DELETE, DROP, etc.) are not permitted."
17670
- );
17671
- }
17672
- }
17673
- function rowToChunkResult(row) {
17674
- const chunkId = row.chunk_id ?? row.id;
17675
- const chunkContent = row.chunk_content ?? row.content;
17676
- const itemId = row.item_id ?? row.source;
17677
- const context = row.context ?? row.context_id;
17678
- const itemName = row.item_name ?? row.name;
17679
- if (!chunkId || !chunkContent && !itemId) return null;
17680
- return {
17681
- item_name: itemName ?? "",
17682
- item_id: itemId ?? "",
17683
- context: context ?? "",
17684
- chunk_id: chunkId,
17685
- chunk_index: row.chunk_index ?? void 0,
17686
- chunk_content: chunkContent ?? void 0,
17687
- metadata: row.metadata ?? row.chunk_metadata ?? void 0
17688
- };
17689
- }
17690
- function createTools(params) {
17691
- const { contexts, user, role, sessionDir } = params;
17692
- let queryCount = 0;
17693
- const execute_query = tool5({
17694
- description: `Execute a read-only PostgreSQL SELECT query against the knowledge base.
17695
-
17696
- Use this to search, filter, aggregate, and explore content. The database contains items
17697
- and chunks tables for each knowledge base (see schema in the system prompt).
17698
-
17699
- Use embed('your text') anywhere in the query to generate a semantic search vector:
17700
- embedding <=> embed('machine learning') AS distance
17701
-
17702
- If the result exceeds ${(MAX_INLINE_CHARS / 1e3).toFixed(0)}k characters it is saved to a file.
17703
- Use the grep tool to iteratively search the file for relevant information.`,
17704
- inputSchema: z18.object({
17705
- sql: z18.string().describe("A read-only SELECT (or WITH ... SELECT) PostgreSQL query")
17706
- }),
17707
- execute: async ({ sql }) => {
17708
- assertReadOnly(sql);
17709
- let processedSql;
17710
- try {
17711
- processedSql = await preprocessEmbedCalls(sql, contexts, user, role);
17712
- } catch (err) {
17713
- return JSON.stringify({ error: `embed() preprocessing failed: ${err.message}` });
17714
- }
17715
- let rows;
17716
- try {
17717
- const { db: db2 } = await postgresClient();
17718
- const result = await db2.raw(processedSql);
17719
- rows = result.rows ?? [];
17720
- } catch (err) {
17721
- return JSON.stringify({ error: `Query failed: ${err.message}` });
17722
- }
17723
- const json = JSON.stringify(rows, null, 2);
17724
- if (json.length <= MAX_INLINE_CHARS) {
17725
- return json;
17726
- }
17727
- await fs6.mkdir(sessionDir, { recursive: true });
17728
- const filename = `query_${++queryCount}.json`;
17729
- const filePath = path3.join(sessionDir, filename);
17730
- await fs6.writeFile(filePath, json, "utf-8");
17731
- return JSON.stringify({
17732
- stored: true,
17733
- file: filePath,
17734
- row_count: rows.length,
17735
- message: `Results too large to display (${rows.length} rows, ${(json.length / 1e3).toFixed(1)}k chars). Stored at ${filePath}. Use the grep tool to search for relevant information.`,
17736
- grep_hint: `grep -i "keyword" ${filePath}`
17737
- });
17738
- }
17739
- });
17740
- const grep = tool5({
17741
- description: `Search a stored query result file using grep.
17742
-
17743
- Use this after execute_query returns a file path because results were too large.
17744
- Iteratively narrow down the results with multiple grep calls.`,
17745
- inputSchema: z18.object({
17746
- pattern: z18.string().describe("Regular expression or literal string to search for"),
17747
- file: z18.string().describe("Absolute path to the file returned by execute_query"),
17748
- context_lines: z18.number().int().min(0).max(10).default(2).describe("Number of lines of context to show around each match (default 2)"),
17749
- case_insensitive: z18.boolean().default(true).describe("Case-insensitive matching (default true)")
17750
- }),
17751
- execute: async ({ pattern, file, context_lines, case_insensitive }) => {
17752
- const resolvedFile = path3.resolve(file);
17753
- const resolvedSession = path3.resolve(sessionDir);
17754
- if (!resolvedFile.startsWith(resolvedSession)) {
17755
- return JSON.stringify({
17756
- error: `Access denied. Only files within the session directory (${sessionDir}) can be searched.`
17757
- });
17758
- }
17759
- try {
17760
- await fs6.access(resolvedFile);
17761
- } catch {
17762
- return JSON.stringify({ error: `File not found: ${file}` });
17763
- }
17764
- const flags = [
17765
- "-n",
17766
- context_lines > 0 ? `-C${context_lines}` : "",
17767
- case_insensitive ? "-i" : ""
17768
- ].filter(Boolean).join(" ");
17769
- const escapedPattern = pattern.replace(/'/g, `'\\''`);
17770
- const cmd = `grep ${flags} '${escapedPattern}' '${resolvedFile}'`;
17771
- let output;
17772
- try {
17773
- const { stdout } = await execAsync3(cmd, { maxBuffer: 10 * 1024 * 1024 });
17774
- output = stdout;
17775
- } catch (err) {
17776
- if (err.code === 1) {
17777
- return JSON.stringify({ matches: 0, output: "No matches found." });
17778
- }
17779
- return JSON.stringify({ error: `grep failed: ${err.message}` });
17780
- }
17781
- if (output.length > MAX_GREP_OUTPUT_CHARS) {
17782
- output = output.slice(0, MAX_GREP_OUTPUT_CHARS) + `
17783
- ... (output truncated at ${MAX_GREP_OUTPUT_CHARS} chars \u2014 refine your pattern to narrow results)`;
17784
- }
17785
- const lineCount = output.split("\n").filter(Boolean).length;
17786
- return JSON.stringify({ matches: lineCount, output });
17787
- }
17788
- });
17789
- return { execute_query, grep };
17790
- }
17791
- function harvestChunks(toolResults) {
17792
- const chunks = [];
17793
- for (const result of toolResults ?? []) {
17794
- const rawOutput = result.output ?? result.result;
17795
- let parsed;
17796
- try {
17797
- parsed = typeof rawOutput === "string" ? JSON.parse(rawOutput) : rawOutput;
17798
- } catch {
17799
- continue;
17800
- }
17801
- if (Array.isArray(parsed)) {
17802
- for (const row of parsed) {
17803
- if (row && typeof row === "object") {
17804
- const chunk = rowToChunkResult(row);
17805
- if (chunk) chunks.push(chunk);
17806
- }
17807
- }
17808
- }
17809
- }
17810
- return chunks;
17811
- }
17812
-
17813
- // ee/agentic-retrieval/v4/system-prompt.ts
17814
- function buildSystemPrompt(contexts, customInstructions) {
17815
- const schemaBlock = buildSchemaBlock(contexts);
17816
- const hasEmbedder = contexts.some((c) => c.embedder != null);
17817
- return `You are a knowledge base retrieval agent. Your job is to find all information relevant to the user's query.
17818
-
17819
- ## Approach: Observe \u2192 Infer \u2192 Act
17820
-
17821
- Work iteratively:
17822
- 1. **Observe** \u2014 examine what data you have and what the query asks for
17823
- 2. **Infer** \u2014 decide what SQL query will best surface relevant information
17824
- 3. **Act** \u2014 execute the query and study the results
17825
- 4. Repeat until you have found sufficient information, then write your final answer.
17826
-
17827
- Do NOT guess or hallucinate. If results are empty, try alternative queries (different keywords,
17828
- broader filters, semantic search). Exhaust the available search strategies before concluding
17829
- that no relevant data exists.
17830
-
17831
- ---
17832
-
17833
- ## Database Schema
17834
-
17835
- ${schemaBlock}
17836
-
17837
- ---
17838
-
17839
- ## Query Patterns
17840
-
17841
- ### Keyword / Full-Text Search
17842
- \`\`\`sql
17843
- SELECT
17844
- c.id AS chunk_id,
17845
- c.chunk_index,
17846
- c.content AS chunk_content,
17847
- c.metadata,
17848
- c.source AS item_id,
17849
- i.name AS item_name,
17850
- '<context_id>' AS context
17851
- FROM <context_id>_chunks c
17852
- JOIN <context_id>_items i ON c.source = i.id
17853
- WHERE c.fts @@ plainto_tsquery('english', 'your search terms')
17854
- AND (i.archived IS FALSE OR i.archived IS NULL)
17855
- ORDER BY ts_rank(c.fts, plainto_tsquery('english', 'your search terms')) DESC
17856
- LIMIT 20;
17857
- \`\`\`
17858
-
17859
- For German text use \`'german'\` instead of \`'english'\`.
17860
- For multi-language, use \`websearch_to_tsquery\` or UNION both languages.
17861
- ${hasEmbedder ? `
17862
- ### Semantic Search (use embed() helper)
17863
- \`\`\`sql
17864
- SELECT
17865
- c.id AS chunk_id,
17866
- c.chunk_index,
17867
- c.content AS chunk_content,
17868
- c.metadata,
17869
- c.source AS item_id,
17870
- i.name AS item_name,
17871
- '<context_id>' AS context,
17872
- c.embedding <=> embed('your concept here') AS distance
17873
- FROM <context_id>_chunks c
17874
- JOIN <context_id>_items i ON c.source = i.id
17875
- WHERE (i.archived IS FALSE OR i.archived IS NULL)
17876
- ORDER BY distance ASC
17877
- LIMIT 20;
17878
- \`\`\`
17879
-
17880
- ### Hybrid Search (keyword + semantic combined via RRF)
17881
- \`\`\`sql
17882
- WITH fts AS (
17883
- SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank(fts, q) DESC) AS rank
17884
- FROM <context_id>_chunks, plainto_tsquery('english', 'your query') q
17885
- WHERE fts @@ q
17886
- LIMIT 500
17887
- ),
17888
- sem AS (
17889
- SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> embed('your query') ASC) AS rank
17890
- FROM <context_id>_chunks
17891
- LIMIT 500
17892
- ),
17893
- rrf AS (
17894
- SELECT
17895
- COALESCE(fts.id, sem.id) AS id,
17896
- (COALESCE(1.0 / (50 + fts.rank), 0) * 2 + COALESCE(1.0 / (50 + sem.rank), 0)) AS score
17897
- FROM fts FULL OUTER JOIN sem ON fts.id = sem.id
17898
- )
17899
- SELECT
17900
- c.id AS chunk_id,
17901
- c.chunk_index,
17902
- c.content AS chunk_content,
17903
- c.metadata,
17904
- c.source AS item_id,
17905
- i.name AS item_name,
17906
- '<context_id>' AS context,
17907
- rrf.score
17908
- FROM rrf
17909
- JOIN <context_id>_chunks c ON c.id = rrf.id
17910
- JOIN <context_id>_items i ON c.source = i.id
17911
- WHERE (i.archived IS FALSE OR i.archived IS NULL)
17912
- ORDER BY rrf.score DESC
17913
- LIMIT 20;
17914
- \`\`\`
17915
- ` : `
17916
- Note: No embedder is configured for these contexts. Use keyword/full-text search only.
17917
- `}
17918
- ### Browse all chunks of a specific document (in order)
17919
- \`\`\`sql
17920
- SELECT
17921
- c.id AS chunk_id,
17922
- c.chunk_index,
17923
- c.content AS chunk_content,
17924
- c.metadata,
17925
- c.source AS item_id,
17926
- i.name AS item_name,
17927
- '<context_id>' AS context
17928
- FROM <context_id>_chunks c
17929
- JOIN <context_id>_items i ON c.source = i.id
17930
- WHERE c.source = '<item_id>'
17931
- ORDER BY c.chunk_index;
17932
- \`\`\`
17933
-
17934
- ### Count / aggregate
17935
- \`\`\`sql
17936
- SELECT COUNT(*) FROM <context_id>_items WHERE archived IS FALSE;
17937
- SELECT COUNT(*) FROM <context_id>_chunks;
17938
- \`\`\`
17939
-
17940
- ### Explore item names (when query is about a specific document)
17941
- \`\`\`sql
17942
- SELECT id, name, external_id, "createdAt"
17943
- FROM <context_id>_items
17944
- WHERE (archived IS FALSE OR archived IS NULL)
17945
- AND LOWER(name) LIKE '%keyword%'
17946
- LIMIT 50;
17947
- \`\`\`
17948
-
17949
- ### Filter by custom metadata on chunks
17950
- \`\`\`sql
17951
- SELECT chunk_id, chunk_content, item_name, context
17952
- FROM ...
17953
- WHERE c.metadata->>'page' = '5'
17954
- OR c.metadata @> '{"category": "finance"}'
17955
- \`\`\`
17956
-
17957
- ---
17958
-
17959
- ## Column Alias Convention
17960
-
17961
- **Always use these aliases** in queries that return chunks so results are collected correctly:
17962
-
17963
- | Alias | Source column |
17964
- |----------------|-------------------------|
17965
- | \`chunk_id\` | \`c.id\` |
17966
- | \`chunk_index\` | \`c.chunk_index\` |
17967
- | \`chunk_content\`| \`c.content\` |
17968
- | \`item_id\` | \`c.source\` |
17969
- | \`item_name\` | \`i.name\` |
17970
- | \`context\` | literal context id string |
17971
- | \`metadata\` | \`c.metadata\` |
17972
-
17973
- ---
17974
-
17975
- ## Handling Large Results
17976
-
17977
- When execute_query returns a file path (results > 20k chars):
17978
- 1. Use \`grep\` with a specific pattern to find relevant sections
17979
- 2. Multiple grep calls are fine \u2014 narrow down iteratively
17980
- 3. Once you know specific \`item_id\` or \`chunk_id\` values, run a targeted SELECT to get full content
17981
-
17982
- ---
17983
-
17984
- ## Search Strategy
17985
-
17986
- - **Start broad**: use keyword or hybrid search with your main terms, LIMIT 30\u201350
17987
- - **Go deeper**: if results are sparse, try alternative phrasings, synonyms, or semantic search
17988
- - **Drill into documents**: once you find a relevant item, fetch its chunks in order to get full context
17989
- - **Cross-context**: search multiple contexts when the query could span knowledge bases
17990
- - **Aggregate last**: use COUNT queries only for "how many" questions
17991
-
17992
- ---
17993
- ${customInstructions ? `## Additional Instructions
17994
-
17995
- ${customInstructions}
17996
-
17997
- ---
17998
- ` : ""}
17999
- When you have gathered sufficient information, write a clear answer. Do not call any more tools once you have what you need.`;
18000
- }
18001
- function buildSchemaBlock(contexts) {
18002
- return contexts.map((ctx) => {
18003
- const itemsTable = getTableName(ctx.id);
18004
- const chunksTable = getChunksTableName(ctx.id);
18005
- const customFields = ctx.fields.length > 0 ? ctx.fields.map((f) => ` ${f.name} (${f.type})`).join("\n") : " (no custom fields)";
18006
- const embedderNote = ctx.embedder ? `Embedder: ${ctx.embedder.name} \u2014 semantic search and embed() are available` : "No embedder \u2014 use keyword search only";
18007
- return `### Context: "${ctx.name}" (id: \`${ctx.id}\`)
18008
- ${ctx.description || ""}
18009
- ${embedderNote}
18010
-
18011
- **${itemsTable}** \u2014 documents / items
18012
- id (uuid, primary key)
18013
- name (text)
18014
- external_id (text, nullable)
18015
- archived (boolean, nullable)
18016
- created_by (integer, nullable)
18017
- rights_mode (text, nullable)
18018
- "createdAt" (timestamp)
18019
- "updatedAt" (timestamp)
18020
- -- Custom fields:
18021
- ${customFields}
18022
-
18023
- **${chunksTable}** \u2014 text chunks (source FK \u2192 ${itemsTable}.id)
18024
- id (uuid, primary key)
18025
- source (uuid, FK \u2192 ${itemsTable}.id)
18026
- content (text)
18027
- chunk_index (integer)
18028
- fts (tsvector \u2014 full-text search index)
18029
- embedding (vector \u2014 pgvector, nullable)
18030
- metadata (jsonb, nullable)
18031
- "createdAt" (timestamp)
18032
- "updatedAt" (timestamp)`;
18033
- }).join("\n\n");
18034
- }
18035
-
18036
- // ee/agentic-retrieval/v4/agent-loop.ts
18037
- import { generateText as generateText6, stepCountIs as stepCountIs3 } from "ai";
18038
- var MAX_STEPS = 10;
18039
- async function* runAgentLoop2(params) {
18040
- const { query, systemPrompt, tools, model, onStepComplete } = params;
18041
- const output = {
18042
- steps: [],
18043
- reasoning: [],
18044
- chunks: [],
18045
- usage: [],
18046
- totalTokens: 0
18047
- };
18048
- const seenChunkIds = /* @__PURE__ */ new Set();
18049
- const messages = [{ role: "user", content: query }];
18050
- for (let step = 0; step < MAX_STEPS; step++) {
18051
- console.log(`[EXULU] v4 agent loop \u2014 step ${step + 1}/${MAX_STEPS}`);
18052
- let result;
18053
- try {
18054
- result = await withRetry(
18055
- () => generateText6({
18056
- model,
18057
- temperature: 0,
18058
- system: systemPrompt,
18059
- messages,
18060
- tools,
18061
- toolChoice: "auto",
18062
- stopWhen: stepCountIs3(1)
18063
- })
18064
- );
18065
- } catch (err) {
18066
- console.error("[EXULU] v4 generateText failed:", err);
18067
- throw err;
18068
- }
18069
- messages.push(...result.response.messages);
18070
- const rawToolResults = result.toolResults ?? [];
18071
- const stepChunks = [];
18072
- for (const chunk of harvestChunks(rawToolResults)) {
18073
- if (!chunk.chunk_id || !seenChunkIds.has(chunk.chunk_id)) {
18074
- if (chunk.chunk_id) seenChunkIds.add(chunk.chunk_id);
18075
- stepChunks.push(chunk);
18076
- }
18077
- }
18078
- const stepRecord = {
18079
- stepNumber: step + 1,
18080
- text: result.text ?? "",
18081
- toolCalls: result.toolCalls?.map((tc) => ({
18082
- name: tc.toolName,
18083
- id: tc.toolCallId,
18084
- input: tc.input
18085
- })) ?? [],
18086
- chunks: stepChunks,
18087
- tokens: result.usage?.totalTokens ?? 0
18088
- };
18089
- output.steps.push(stepRecord);
18090
- output.reasoning.push({
18091
- text: result.text ?? "",
18092
- tools: result.toolCalls?.map((tc) => ({
18093
- name: tc.toolName,
18094
- id: tc.toolCallId,
18095
- input: tc.input,
18096
- output: rawToolResults.find(
18097
- (r) => (r.toolCallId ?? r.id) === tc.toolCallId
18098
- )?.output
18099
- })) ?? []
18100
- });
18101
- output.chunks.push(...stepChunks);
18102
- output.usage.push(result.usage);
18103
- onStepComplete?.(stepRecord);
18104
- yield { ...output };
18105
- const calledTools = result.toolCalls?.length > 0;
18106
- if (!calledTools) {
18107
- console.log(`[EXULU] v4 \u2014 model finished after step ${step + 1} (no tool calls)`);
18108
- break;
18109
- }
18110
- }
18111
- output.totalTokens = output.usage.reduce((sum, u) => sum + (u?.totalTokens ?? 0), 0);
18112
- }
18113
-
18114
- // ee/agentic-retrieval/v4/index.ts
18115
- async function* executeV4({
18116
- query,
18117
- contexts,
18118
- model,
18119
- user,
18120
- role,
18121
- customInstructions
18122
- }) {
18123
- const sessionId = randomUUID8();
18124
- const sessionDir = path4.join(os.tmpdir(), `exulu-v4-${sessionId}`);
18125
- console.log("[EXULU] v4 \u2014 starting observe-infer-act retrieval");
18126
- const tools = createTools({ contexts, user, role, sessionDir });
18127
- const systemPrompt = buildSystemPrompt(contexts, customInstructions);
18128
- let finalOutput;
18129
- try {
18130
- for await (const output of runAgentLoop2({
18131
- query,
18132
- systemPrompt,
18133
- tools,
18134
- model
18135
- })) {
18136
- finalOutput = output;
18137
- yield output;
18138
- }
18139
- } finally {
18140
- fs7.rm(sessionDir, { recursive: true, force: true }).catch(() => {
18141
- });
18142
- }
18143
- if (finalOutput) {
18144
- console.log(
18145
- `[EXULU] v4 \u2014 done. steps=${finalOutput.steps.length} chunks=${finalOutput.chunks.length} tokens=${finalOutput.totalTokens}`
18146
- );
18147
- }
18148
- }
18149
- function createAgenticRetrievalToolV4({
18150
- contexts,
18151
- instructions: adminInstructions,
18152
- rerankers,
18153
- user,
18154
- role,
18155
- model
18156
- }) {
18157
- const license = checkLicense();
18158
- if (!license["agentic-retrieval"]) {
18159
- console.warn("[EXULU] Not licensed for agentic retrieval");
18160
- return void 0;
18161
- }
18162
- const contextNames = contexts.map((c) => c.id).join(", ");
18163
- return new ExuluTool({
18164
- id: "agentic_context_search_v4",
18165
- name: "Agentic Context Search (V4)",
18166
- description: `Observe-infer-act retrieval using raw SQL. Searches: ${contextNames}`,
18167
- category: "contexts",
18168
- needsApproval: false,
18169
- type: "context",
18170
- config: [
18171
- {
18172
- name: "instructions",
18173
- description: "Custom instructions for the retrieval agent",
18174
- type: "string",
18175
- default: ""
18176
- },
18177
- {
18178
- name: "reasoning_model",
18179
- description: "Override the model used by the retrieval agent (default: inherits from calling agent)",
18180
- type: "string",
18181
- default: ""
18182
- },
18183
- ...contexts.map((ctx) => ({
18184
- name: ctx.id,
18185
- description: `Enable search in "${ctx.name}". ${ctx.description}`,
18186
- type: "boolean",
18187
- default: true
18188
- }))
18189
- ],
18190
- inputSchema: z19.object({
18191
- query: z19.string().describe("The question or query to answer"),
18192
- userInstructions: z19.string().optional().describe("Additional instructions from the user to guide retrieval")
18193
- }),
18194
- execute: async function* ({
18195
- query,
18196
- userInstructions,
18197
- toolVariablesConfig
18198
- }) {
18199
- if (!model) {
18200
- throw new Error("Model is required for executing the agentic retrieval tool");
18201
- }
18202
- let activeContexts = contexts;
18203
- let configInstructions = "";
18204
- if (toolVariablesConfig) {
18205
- configInstructions = toolVariablesConfig["instructions"] ?? "";
18206
- activeContexts = contexts.filter(
18207
- (ctx) => toolVariablesConfig[ctx.id] === true || toolVariablesConfig[ctx.id] === "true" || toolVariablesConfig[ctx.id] === 1
18208
- );
18209
- if (activeContexts.length === 0) activeContexts = contexts;
18210
- }
18211
- const combinedInstructions = [
18212
- configInstructions ? `Configuration instructions: ${configInstructions}` : "",
18213
- adminInstructions ? `Admin instructions: ${adminInstructions}` : "",
18214
- userInstructions ? `User instructions: ${userInstructions}` : ""
18215
- ].filter(Boolean).join("\n");
18216
- for await (const output of executeV4({
18217
- query,
18218
- contexts: activeContexts,
18219
- model,
18220
- user,
18221
- role,
18222
- customInstructions: combinedInstructions || void 0
18223
- })) {
18224
- yield { result: JSON.stringify(output) };
18225
- }
18226
- }
18227
- });
18228
- }
18229
-
18230
19030
  // src/index.ts
18231
19031
  var ExuluJobs = {
18232
19032
  redis: redisClient
@@ -18235,8 +19035,7 @@ var ExuluDefaultTools = {
18235
19035
  agentic: {
18236
19036
  retrieval: {
18237
19037
  create: {
18238
- v3: createAgenticRetrievalToolV3,
18239
- v4: createAgenticRetrievalToolV4
19038
+ v3: createAgenticRetrievalToolV3
18240
19039
  }
18241
19040
  }
18242
19041
  }
@@ -18277,8 +19076,8 @@ var ExuluVariables = {
18277
19076
  throw new Error(`Variable ${name} not found.`);
18278
19077
  }
18279
19078
  if (variable.encrypted) {
18280
- const bytes = CryptoJS9.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
18281
- variable.value = bytes.toString(CryptoJS9.enc.Utf8);
19079
+ const bytes = CryptoJS10.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
19080
+ variable.value = bytes.toString(CryptoJS10.enc.Utf8);
18282
19081
  }
18283
19082
  return variable.value;
18284
19083
  }