@exulu/backend 1.37.0 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
- # [1.37.0](https://github.com/Qventu/exulu-backend/compare/v1.36.1...v1.37.0) (2025-11-26)
1
+ # [1.38.0](https://github.com/Qventu/exulu-backend/compare/v1.37.0...v1.38.0) (2025-11-30)
2
2
 
3
3
 
4
4
  ### Features
5
5
 
6
- * add PDF preview tool and enhance agent API with cookie authentication and message persistence ([9b4ea81](https://github.com/Qventu/exulu-backend/commit/9b4ea81636ad445021ced8ff7983be001e93eeb9))
6
+ * add document parsing for AI agent file handling with JWT secret fix ([fc97b5f](https://github.com/Qventu/exulu-backend/commit/fc97b5ff14c617350a130a12a6077c5fc23fda8b))
package/dist/index.cjs CHANGED
@@ -463,7 +463,9 @@ var getToken = async (authHeader) => {
463
463
  }
464
464
  try {
465
465
  const secret = process.env.NEXTAUTH_SECRET;
466
- const jwk = await (0, import_jose.importJWK)({ k: secret, alg: "HS256", kty: "oct" });
466
+ const secretBuffer = Buffer.from(secret, "utf-8");
467
+ const base64Secret = secretBuffer.toString("base64url");
468
+ const jwk = await (0, import_jose.importJWK)({ k: base64Secret, alg: "HS256", kty: "oct" });
467
469
  const { payload } = await (0, import_jose.jwtVerify)(token, jwk);
468
470
  return payload;
469
471
  } catch (error) {
@@ -4969,6 +4971,7 @@ var createUppyRoutes = async (app, config) => {
4969
4971
  };
4970
4972
 
4971
4973
  // src/registry/classes.ts
4974
+ var import_officeparser = require("officeparser");
4972
4975
  var s3Client2;
4973
4976
  function sanitizeToolName(name) {
4974
4977
  if (typeof name !== "string") return "";
@@ -5482,6 +5485,71 @@ var ExuluAgent2 = class {
5482
5485
  }
5483
5486
  return "";
5484
5487
  };
5488
+ /**
5489
+ * Convert file parts in messages to OpenAI Responses API compatible format.
5490
+ * The OpenAI Responses API doesn't support inline file parts with type 'file'.
5491
+ * This function converts:
5492
+ * - Document files (PDF, DOCX, etc.) -> text parts with extracted content using officeparser
5493
+ * - Image files -> image parts (which ARE supported by Responses API)
5494
+ */
5495
+ async processFilePartsInMessages(messages) {
5496
+ const processedMessages = await Promise.all(messages.map(async (message) => {
5497
+ console.log("[EXULU] Processing file parts in messages: " + JSON.stringify(message, null, 2));
5498
+ if (message.role !== "user" || !Array.isArray(message.parts)) {
5499
+ return message;
5500
+ }
5501
+ const processedParts = await Promise.all(message.parts.map(async (part) => {
5502
+ if (part.type !== "file") {
5503
+ return part;
5504
+ }
5505
+ const { mediaType, url, filename } = part;
5506
+ const imageTypes = ["image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp"];
5507
+ if (imageTypes.includes(mediaType)) {
5508
+ console.log(`[EXULU] Converting file part to image part: ${filename}`);
5509
+ return {
5510
+ type: "image",
5511
+ image: url,
5512
+ mimeType: mediaType
5513
+ };
5514
+ }
5515
+ console.log(`[EXULU] Converting file part to text using officeparser: ${filename}`);
5516
+ try {
5517
+ const response = await fetch(url);
5518
+ if (!response.ok) {
5519
+ console.error(`[EXULU] Failed to fetch file: ${filename}, status: ${response.status}`);
5520
+ return {
5521
+ type: "text",
5522
+ text: `[Error: Could not load file ${filename}]`
5523
+ };
5524
+ }
5525
+ const arrayBuffer = await response.arrayBuffer();
5526
+ const extractedText = await (0, import_officeparser.parseOfficeAsync)(arrayBuffer, {
5527
+ outputErrorToConsole: false,
5528
+ newlineDelimiter: "\n"
5529
+ });
5530
+ return {
5531
+ type: "text",
5532
+ text: `<file name="${filename}">
5533
+ ${extractedText}
5534
+ </file>`
5535
+ };
5536
+ } catch (error) {
5537
+ console.error(`[EXULU] Error processing file ${filename}:`, error);
5538
+ return {
5539
+ type: "text",
5540
+ text: `[Error extracting text from file ${filename}: ${error instanceof Error ? error.message : "Unknown error"}]`
5541
+ };
5542
+ }
5543
+ }));
5544
+ const result = {
5545
+ ...message,
5546
+ parts: processedParts
5547
+ };
5548
+ console.log("[EXULU] Result: " + JSON.stringify(result, null, 2));
5549
+ return result;
5550
+ }));
5551
+ return processedMessages;
5552
+ }
5485
5553
  generateStream = async ({
5486
5554
  user,
5487
5555
  session,
@@ -5529,6 +5597,11 @@ var ExuluAgent2 = class {
5529
5597
  // append the new message to the previous messages:
5530
5598
  messages: [...previousMessagesContent, message]
5531
5599
  });
5600
+ messages = messages.filter(
5601
+ (message2, index, self) => index === self.findIndex((t) => t.id === message2.id)
5602
+ );
5603
+ console.log("[EXULU] Processing file parts in messages for OpenAI Responses API compatibility");
5604
+ messages = await this.processFilePartsInMessages(messages);
5532
5605
  const genericContext = "IMPORTANT: \n\n The current date is " + (/* @__PURE__ */ new Date()).toLocaleDateString() + " and the current time is " + (/* @__PURE__ */ new Date()).toLocaleTimeString() + ". If the user does not explicitly provide the current date, for examle when saying ' this weekend', you should assume they are talking with the current date in mind as a reference.";
5533
5606
  let system = instructions || "You are a helpful assistant. When you use a tool to answer a question do not explicitly comment on the result of the tool call unless the user has explicitly you to do something with the result.";
5534
5607
  system += "\n\n" + genericContext;
@@ -5593,7 +5666,7 @@ var getSession = async ({ sessionID }) => {
5593
5666
  };
5594
5667
  var saveChat = async ({ session, user, messages }) => {
5595
5668
  const { db: db3 } = await postgresClient();
5596
- const promises = messages.map((message) => {
5669
+ for (const message of messages) {
5597
5670
  const mutation = db3.from("agent_messages").insert({
5598
5671
  session,
5599
5672
  user,
@@ -5602,9 +5675,8 @@ var saveChat = async ({ session, user, messages }) => {
5602
5675
  title: message.role === "user" ? "User" : "Assistant"
5603
5676
  }).returning("id");
5604
5677
  mutation.onConflict("message_id").merge();
5605
- return mutation;
5606
- });
5607
- await Promise.all(promises);
5678
+ await mutation;
5679
+ }
5608
5680
  };
5609
5681
  var ExuluEmbedder = class {
5610
5682
  id;
package/dist/index.d.cts CHANGED
@@ -398,6 +398,14 @@ declare class ExuluAgent {
398
398
  instructions?: string;
399
399
  outputSchema?: z.ZodType;
400
400
  }) => Promise<string | any>;
401
+ /**
402
+ * Convert file parts in messages to OpenAI Responses API compatible format.
403
+ * The OpenAI Responses API doesn't support inline file parts with type 'file'.
404
+ * This function converts:
405
+ * - Document files (PDF, DOCX, etc.) -> text parts with extracted content using officeparser
406
+ * - Image files -> image parts (which ARE supported by Responses API)
407
+ */
408
+ private processFilePartsInMessages;
401
409
  generateStream: ({ user, session, message, previousMessages, currentTools, allExuluTools, toolConfigs, providerapikey, contexts, exuluConfig, instructions, req, }: {
402
410
  user?: User;
403
411
  session?: string;
package/dist/index.d.ts CHANGED
@@ -398,6 +398,14 @@ declare class ExuluAgent {
398
398
  instructions?: string;
399
399
  outputSchema?: z.ZodType;
400
400
  }) => Promise<string | any>;
401
+ /**
402
+ * Convert file parts in messages to OpenAI Responses API compatible format.
403
+ * The OpenAI Responses API doesn't support inline file parts with type 'file'.
404
+ * This function converts:
405
+ * - Document files (PDF, DOCX, etc.) -> text parts with extracted content using officeparser
406
+ * - Image files -> image parts (which ARE supported by Responses API)
407
+ */
408
+ private processFilePartsInMessages;
401
409
  generateStream: ({ user, session, message, previousMessages, currentTools, allExuluTools, toolConfigs, providerapikey, contexts, exuluConfig, instructions, req, }: {
402
410
  user?: User;
403
411
  session?: string;
package/dist/index.js CHANGED
@@ -411,7 +411,9 @@ var getToken = async (authHeader) => {
411
411
  }
412
412
  try {
413
413
  const secret = process.env.NEXTAUTH_SECRET;
414
- const jwk = await importJWK({ k: secret, alg: "HS256", kty: "oct" });
414
+ const secretBuffer = Buffer.from(secret, "utf-8");
415
+ const base64Secret = secretBuffer.toString("base64url");
416
+ const jwk = await importJWK({ k: base64Secret, alg: "HS256", kty: "oct" });
415
417
  const { payload } = await jwtVerify(token, jwk);
416
418
  return payload;
417
419
  } catch (error) {
@@ -4936,6 +4938,7 @@ var createUppyRoutes = async (app, config) => {
4936
4938
  };
4937
4939
 
4938
4940
  // src/registry/classes.ts
4941
+ import { parseOfficeAsync } from "officeparser";
4939
4942
  var s3Client2;
4940
4943
  function sanitizeToolName(name) {
4941
4944
  if (typeof name !== "string") return "";
@@ -5449,6 +5452,71 @@ var ExuluAgent2 = class {
5449
5452
  }
5450
5453
  return "";
5451
5454
  };
5455
+ /**
5456
+ * Convert file parts in messages to OpenAI Responses API compatible format.
5457
+ * The OpenAI Responses API doesn't support inline file parts with type 'file'.
5458
+ * This function converts:
5459
+ * - Document files (PDF, DOCX, etc.) -> text parts with extracted content using officeparser
5460
+ * - Image files -> image parts (which ARE supported by Responses API)
5461
+ */
5462
+ async processFilePartsInMessages(messages) {
5463
+ const processedMessages = await Promise.all(messages.map(async (message) => {
5464
+ console.log("[EXULU] Processing file parts in messages: " + JSON.stringify(message, null, 2));
5465
+ if (message.role !== "user" || !Array.isArray(message.parts)) {
5466
+ return message;
5467
+ }
5468
+ const processedParts = await Promise.all(message.parts.map(async (part) => {
5469
+ if (part.type !== "file") {
5470
+ return part;
5471
+ }
5472
+ const { mediaType, url, filename } = part;
5473
+ const imageTypes = ["image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp"];
5474
+ if (imageTypes.includes(mediaType)) {
5475
+ console.log(`[EXULU] Converting file part to image part: ${filename}`);
5476
+ return {
5477
+ type: "image",
5478
+ image: url,
5479
+ mimeType: mediaType
5480
+ };
5481
+ }
5482
+ console.log(`[EXULU] Converting file part to text using officeparser: ${filename}`);
5483
+ try {
5484
+ const response = await fetch(url);
5485
+ if (!response.ok) {
5486
+ console.error(`[EXULU] Failed to fetch file: ${filename}, status: ${response.status}`);
5487
+ return {
5488
+ type: "text",
5489
+ text: `[Error: Could not load file ${filename}]`
5490
+ };
5491
+ }
5492
+ const arrayBuffer = await response.arrayBuffer();
5493
+ const extractedText = await parseOfficeAsync(arrayBuffer, {
5494
+ outputErrorToConsole: false,
5495
+ newlineDelimiter: "\n"
5496
+ });
5497
+ return {
5498
+ type: "text",
5499
+ text: `<file name="${filename}">
5500
+ ${extractedText}
5501
+ </file>`
5502
+ };
5503
+ } catch (error) {
5504
+ console.error(`[EXULU] Error processing file ${filename}:`, error);
5505
+ return {
5506
+ type: "text",
5507
+ text: `[Error extracting text from file ${filename}: ${error instanceof Error ? error.message : "Unknown error"}]`
5508
+ };
5509
+ }
5510
+ }));
5511
+ const result = {
5512
+ ...message,
5513
+ parts: processedParts
5514
+ };
5515
+ console.log("[EXULU] Result: " + JSON.stringify(result, null, 2));
5516
+ return result;
5517
+ }));
5518
+ return processedMessages;
5519
+ }
5452
5520
  generateStream = async ({
5453
5521
  user,
5454
5522
  session,
@@ -5496,6 +5564,11 @@ var ExuluAgent2 = class {
5496
5564
  // append the new message to the previous messages:
5497
5565
  messages: [...previousMessagesContent, message]
5498
5566
  });
5567
+ messages = messages.filter(
5568
+ (message2, index, self) => index === self.findIndex((t) => t.id === message2.id)
5569
+ );
5570
+ console.log("[EXULU] Processing file parts in messages for OpenAI Responses API compatibility");
5571
+ messages = await this.processFilePartsInMessages(messages);
5499
5572
  const genericContext = "IMPORTANT: \n\n The current date is " + (/* @__PURE__ */ new Date()).toLocaleDateString() + " and the current time is " + (/* @__PURE__ */ new Date()).toLocaleTimeString() + ". If the user does not explicitly provide the current date, for examle when saying ' this weekend', you should assume they are talking with the current date in mind as a reference.";
5500
5573
  let system = instructions || "You are a helpful assistant. When you use a tool to answer a question do not explicitly comment on the result of the tool call unless the user has explicitly you to do something with the result.";
5501
5574
  system += "\n\n" + genericContext;
@@ -5560,7 +5633,7 @@ var getSession = async ({ sessionID }) => {
5560
5633
  };
5561
5634
  var saveChat = async ({ session, user, messages }) => {
5562
5635
  const { db: db3 } = await postgresClient();
5563
- const promises = messages.map((message) => {
5636
+ for (const message of messages) {
5564
5637
  const mutation = db3.from("agent_messages").insert({
5565
5638
  session,
5566
5639
  user,
@@ -5569,9 +5642,8 @@ var saveChat = async ({ session, user, messages }) => {
5569
5642
  title: message.role === "user" ? "User" : "Assistant"
5570
5643
  }).returning("id");
5571
5644
  mutation.onConflict("message_id").merge();
5572
- return mutation;
5573
- });
5574
- await Promise.all(promises);
5645
+ await mutation;
5646
+ }
5575
5647
  };
5576
5648
  var ExuluEmbedder = class {
5577
5649
  id;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "1.37.0",
4
+ "version": "1.38.0",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {
@@ -90,6 +90,7 @@
90
90
  "jsonwebtoken": "^9.0.2",
91
91
  "knex": "^3.1.0",
92
92
  "link": "^2.1.1",
93
+ "officeparser": "^5.2.2",
93
94
  "openai": "^5.21.0",
94
95
  "papaparse": "^5.5.2",
95
96
  "pg": "^8.16.3",