@aj-archipelago/cortex 1.4.2 → 1.4.3

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.
Files changed (86) hide show
  1. package/README.md +1 -0
  2. package/config.js +1 -1
  3. package/helper-apps/cortex-autogen2/.dockerignore +1 -0
  4. package/helper-apps/cortex-autogen2/Dockerfile +6 -10
  5. package/helper-apps/cortex-autogen2/Dockerfile.worker +2 -0
  6. package/helper-apps/cortex-autogen2/agents.py +203 -2
  7. package/helper-apps/cortex-autogen2/main.py +1 -1
  8. package/helper-apps/cortex-autogen2/pyproject.toml +12 -0
  9. package/helper-apps/cortex-autogen2/requirements.txt +14 -0
  10. package/helper-apps/cortex-autogen2/services/redis_publisher.py +1 -1
  11. package/helper-apps/cortex-autogen2/services/run_analyzer.py +1 -1
  12. package/helper-apps/cortex-autogen2/task_processor.py +431 -229
  13. package/helper-apps/cortex-autogen2/test_entity_fetcher.py +305 -0
  14. package/helper-apps/cortex-autogen2/tests/README.md +240 -0
  15. package/helper-apps/cortex-autogen2/tests/TEST_REPORT.md +342 -0
  16. package/helper-apps/cortex-autogen2/tests/__init__.py +8 -0
  17. package/helper-apps/cortex-autogen2/tests/analysis/__init__.py +1 -0
  18. package/helper-apps/cortex-autogen2/tests/analysis/improvement_suggester.py +224 -0
  19. package/helper-apps/cortex-autogen2/tests/analysis/trend_analyzer.py +211 -0
  20. package/helper-apps/cortex-autogen2/tests/cli/__init__.py +1 -0
  21. package/helper-apps/cortex-autogen2/tests/cli/run_tests.py +296 -0
  22. package/helper-apps/cortex-autogen2/tests/collectors/__init__.py +1 -0
  23. package/helper-apps/cortex-autogen2/tests/collectors/log_collector.py +252 -0
  24. package/helper-apps/cortex-autogen2/tests/collectors/progress_collector.py +182 -0
  25. package/helper-apps/cortex-autogen2/tests/conftest.py +15 -0
  26. package/helper-apps/cortex-autogen2/tests/database/__init__.py +1 -0
  27. package/helper-apps/cortex-autogen2/tests/database/repository.py +501 -0
  28. package/helper-apps/cortex-autogen2/tests/database/schema.sql +108 -0
  29. package/helper-apps/cortex-autogen2/tests/evaluators/__init__.py +1 -0
  30. package/helper-apps/cortex-autogen2/tests/evaluators/llm_scorer.py +294 -0
  31. package/helper-apps/cortex-autogen2/tests/evaluators/prompts.py +250 -0
  32. package/helper-apps/cortex-autogen2/tests/evaluators/wordcloud_validator.py +168 -0
  33. package/helper-apps/cortex-autogen2/tests/metrics/__init__.py +1 -0
  34. package/helper-apps/cortex-autogen2/tests/metrics/collector.py +155 -0
  35. package/helper-apps/cortex-autogen2/tests/orchestrator.py +576 -0
  36. package/helper-apps/cortex-autogen2/tests/test_cases.yaml +279 -0
  37. package/helper-apps/cortex-autogen2/tests/test_data.db +0 -0
  38. package/helper-apps/cortex-autogen2/tests/utils/__init__.py +3 -0
  39. package/helper-apps/cortex-autogen2/tests/utils/connectivity.py +112 -0
  40. package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +74 -24
  41. package/helper-apps/cortex-autogen2/tools/entity_api_registry.json +38 -0
  42. package/helper-apps/cortex-autogen2/tools/file_tools.py +1 -1
  43. package/helper-apps/cortex-autogen2/tools/search_tools.py +436 -238
  44. package/helper-apps/cortex-file-handler/package-lock.json +2 -2
  45. package/helper-apps/cortex-file-handler/package.json +1 -1
  46. package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +4 -5
  47. package/helper-apps/cortex-file-handler/src/blobHandler.js +36 -144
  48. package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +5 -3
  49. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +34 -1
  50. package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +22 -0
  51. package/helper-apps/cortex-file-handler/src/services/storage/LocalStorageProvider.js +28 -1
  52. package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +29 -4
  53. package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +11 -0
  54. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +1 -1
  55. package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +3 -2
  56. package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +8 -1
  57. package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +5 -2
  58. package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +14 -7
  59. package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +5 -2
  60. package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +31 -19
  61. package/package.json +1 -1
  62. package/server/modelExecutor.js +4 -0
  63. package/server/plugins/claude4VertexPlugin.js +540 -0
  64. package/server/plugins/openAiWhisperPlugin.js +43 -2
  65. package/tests/integration/rest/vendors/claude_streaming.test.js +121 -0
  66. package/tests/unit/plugins/claude4VertexPlugin.test.js +462 -0
  67. package/tests/unit/plugins/claude4VertexToolConversion.test.js +413 -0
  68. package/helper-apps/cortex-autogen/.funcignore +0 -8
  69. package/helper-apps/cortex-autogen/Dockerfile +0 -10
  70. package/helper-apps/cortex-autogen/OAI_CONFIG_LIST +0 -6
  71. package/helper-apps/cortex-autogen/agents.py +0 -493
  72. package/helper-apps/cortex-autogen/agents_extra.py +0 -14
  73. package/helper-apps/cortex-autogen/config.py +0 -18
  74. package/helper-apps/cortex-autogen/data_operations.py +0 -29
  75. package/helper-apps/cortex-autogen/function_app.py +0 -44
  76. package/helper-apps/cortex-autogen/host.json +0 -15
  77. package/helper-apps/cortex-autogen/main.py +0 -38
  78. package/helper-apps/cortex-autogen/prompts.py +0 -196
  79. package/helper-apps/cortex-autogen/prompts_extra.py +0 -5
  80. package/helper-apps/cortex-autogen/requirements.txt +0 -9
  81. package/helper-apps/cortex-autogen/search.py +0 -85
  82. package/helper-apps/cortex-autogen/test.sh +0 -40
  83. package/helper-apps/cortex-autogen/tools/sasfileuploader.py +0 -66
  84. package/helper-apps/cortex-autogen/utils.py +0 -88
  85. package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +0 -22
  86. package/helper-apps/cortex-autogen2/poetry.lock +0 -3652
@@ -71,11 +71,14 @@ test.after.always(async (t) => {
71
71
 
72
72
  // Test container parameter validation
73
73
  test("should validate container names correctly", (t) => {
74
+ // Get current container names
75
+ const currentContainers = AZURE_STORAGE_CONTAINER_NAMES;
76
+
74
77
  // Test with valid container names from configuration
75
- AZURE_STORAGE_CONTAINER_NAMES.forEach(containerName => {
78
+ currentContainers.forEach(containerName => {
76
79
  t.true(isValidContainerName(containerName), `${containerName} should be valid`);
77
80
  });
78
-
81
+
79
82
  // Test with invalid container names
80
83
  const invalidNames = ["invalid-container", "", null, undefined, "nonexistent"];
81
84
  invalidNames.forEach(name => {
@@ -123,19 +123,27 @@ test("should get azure provider with specific container name", async (t) => {
123
123
  return;
124
124
  }
125
125
 
126
- const factory = new StorageFactory();
127
-
128
- // Mock the blobHandler constants for this test
129
- const mockConstants = {
130
- AZURE_STORAGE_CONTAINER_NAMES: ["container1", "container2", "container3"],
131
- DEFAULT_AZURE_STORAGE_CONTAINER_NAME: "container1",
132
- isValidContainerName: (name) => ["container1", "container2", "container3"].includes(name)
133
- };
126
+ // Save original env value
127
+ const originalEnv = process.env.AZURE_STORAGE_CONTAINER_NAME;
134
128
 
135
- // Test with valid container name
136
- const provider = await factory.getAzureProvider("container2");
137
- t.truthy(provider);
138
- t.is(provider.containerName, "container2");
129
+ try {
130
+ // Set test container names in environment
131
+ process.env.AZURE_STORAGE_CONTAINER_NAME = "container1,container2,container3";
132
+
133
+ const factory = new StorageFactory();
134
+
135
+ // Test with valid container name
136
+ const provider = await factory.getAzureProvider("container2");
137
+ t.truthy(provider);
138
+ t.is(provider.containerName, "container2");
139
+ } finally {
140
+ // Restore original env
141
+ if (originalEnv) {
142
+ process.env.AZURE_STORAGE_CONTAINER_NAME = originalEnv;
143
+ } else {
144
+ delete process.env.AZURE_STORAGE_CONTAINER_NAME;
145
+ }
146
+ }
139
147
  });
140
148
 
141
149
  test("should throw error for invalid container name", async (t) => {
@@ -159,21 +167,25 @@ test("should cache providers by container name", async (t) => {
159
167
  return;
160
168
  }
161
169
 
162
- const factory = new StorageFactory();
163
-
164
- // Mock valid container names for testing
170
+ // Save original env value
165
171
  const originalEnv = process.env.AZURE_STORAGE_CONTAINER_NAME;
166
- process.env.AZURE_STORAGE_CONTAINER_NAME = "test1,test2,test3";
167
172
 
168
173
  try {
169
- const provider1 = await factory.getAzureProvider("test1");
170
- const provider2 = await factory.getAzureProvider("test1");
171
- const provider3 = await factory.getAzureProvider("test2");
174
+ // Set test container names in environment
175
+ process.env.AZURE_STORAGE_CONTAINER_NAME = "container1,container2,container3";
176
+
177
+ const factory = new StorageFactory();
178
+
179
+ const provider1 = await factory.getAzureProvider("container1");
180
+ const provider2 = await factory.getAzureProvider("container1");
181
+ const provider3 = await factory.getAzureProvider("container2");
172
182
 
173
183
  // Same container should return same instance
174
184
  t.is(provider1, provider2);
175
185
  // Different container should return different instance
176
186
  t.not(provider1, provider3);
187
+ t.is(provider1.containerName, "container1");
188
+ t.is(provider3.containerName, "container2");
177
189
  } finally {
178
190
  // Restore original env
179
191
  if (originalEnv) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "private": false,
6
6
  "repository": {
@@ -24,6 +24,7 @@ import Gemini15VisionPlugin from './plugins/gemini15VisionPlugin.js';
24
24
  import Gemini25ImagePlugin from './plugins/gemini25ImagePlugin.js';
25
25
  import AzureBingPlugin from './plugins/azureBingPlugin.js';
26
26
  import Claude3VertexPlugin from './plugins/claude3VertexPlugin.js';
27
+ import Claude4VertexPlugin from './plugins/claude4VertexPlugin.js';
27
28
  import NeuralSpacePlugin from './plugins/neuralSpacePlugin.js';
28
29
  import RunwareAiPlugin from './plugins/runwareAiPlugin.js';
29
30
  import ReplicateApiPlugin from './plugins/replicateApiPlugin.js';
@@ -113,6 +114,9 @@ class ModelExecutor {
113
114
  case 'CLAUDE-3-VERTEX':
114
115
  plugin = new Claude3VertexPlugin(pathway, model);
115
116
  break;
117
+ case 'CLAUDE-4-VERTEX':
118
+ plugin = new Claude4VertexPlugin(pathway, model);
119
+ break;
116
120
  case 'RUNWARE-AI':
117
121
  plugin = new RunwareAiPlugin(pathway, model);
118
122
  break;
@@ -0,0 +1,540 @@
1
+ import Claude3VertexPlugin from "./claude3VertexPlugin.js";
2
+ import logger from "../../lib/logger.js";
3
+ import axios from 'axios';
4
+
5
+ // Claude 4 default maximum file size limit (30MB) for both images and PDFs
6
+ const CLAUDE4_DEFAULT_MAX_FILE_SIZE = 30 * 1024 * 1024; // 30MB
7
+
8
+ // Helper function to detect file type from URL or content
9
+ function detectFileType(url, contentType) {
10
+ const lowerUrl = url.toLowerCase();
11
+
12
+ // Check for data URLs first and extract media type
13
+ if (lowerUrl.startsWith('data:')) {
14
+ const match = lowerUrl.match(/data:([^;,]+)/);
15
+ if (match) {
16
+ const mediaType = match[1];
17
+ if (mediaType.includes('pdf')) return 'pdf';
18
+ if (mediaType.includes('text') || mediaType.includes('markdown')) return 'text';
19
+ }
20
+ }
21
+
22
+ // Check URL extension - extract path before query string/fragment and check if it ends with extension
23
+ // Remove query string and fragment for more accurate extension detection
24
+ const urlPath = lowerUrl.split('?')[0].split('#')[0];
25
+ if (urlPath.endsWith('.pdf')) return 'pdf';
26
+ if (urlPath.endsWith('.txt') || urlPath.endsWith('.text')) return 'text';
27
+ if (urlPath.endsWith('.md') || urlPath.endsWith('.markdown')) return 'text';
28
+
29
+ // Check content type parameter
30
+ if (contentType) {
31
+ if (contentType.includes('pdf')) return 'pdf';
32
+ if (contentType.includes('text/plain') || contentType.includes('text/markdown')) return 'text';
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ // Fetch image and convert to base64 data URL (copy from parent)
39
+ async function fetchImageAsDataURL(imageUrl) {
40
+ try {
41
+ const dataResponse = await axios.get(imageUrl, {
42
+ timeout: 30000,
43
+ responseType: 'arraybuffer',
44
+ maxRedirects: 5
45
+ });
46
+
47
+ const contentType = dataResponse.headers['content-type'];
48
+ const base64Image = Buffer.from(dataResponse.data).toString('base64');
49
+ return `data:${contentType};base64,${base64Image}`;
50
+ } catch (e) {
51
+ logger.error(`Failed to fetch image: ${imageUrl}. ${e}`);
52
+ throw e;
53
+ }
54
+ }
55
+
56
+ // Fetch file and convert to base64
57
+ async function fetchFileAsDataURL(fileUrl, fileType) {
58
+ try {
59
+ const dataResponse = await axios.get(fileUrl, {
60
+ timeout: 30000,
61
+ responseType: 'arraybuffer',
62
+ maxRedirects: 5
63
+ });
64
+
65
+ const contentType = dataResponse.headers['content-type'];
66
+ const base64Data = Buffer.from(dataResponse.data).toString('base64');
67
+
68
+ // Return appropriate data URL format
69
+ if (fileType === 'pdf') {
70
+ return `data:application/pdf;base64,${base64Data}`;
71
+ } else if (fileType === 'text') {
72
+ return `data:text/plain;base64,${base64Data}`;
73
+ }
74
+
75
+ return `data:${contentType};base64,${base64Data}`;
76
+ } catch (e) {
77
+ logger.error(`Failed to fetch file: ${fileUrl}. ${e}`);
78
+ throw e;
79
+ }
80
+ }
81
+
82
+ // Fetch text file and return as plain text
83
+ async function fetchTextFileAsString(fileUrl) {
84
+ try {
85
+ const dataResponse = await axios.get(fileUrl, {
86
+ timeout: 30000,
87
+ responseType: 'text',
88
+ maxRedirects: 5
89
+ });
90
+
91
+ return dataResponse.data;
92
+ } catch (e) {
93
+ logger.error(`Failed to fetch text file: ${fileUrl}. ${e}`);
94
+ throw e;
95
+ }
96
+ }
97
+
98
+ // Extended convertContentItem function that handles PDFs and text files
99
+ async function convertContentItemClaude4(item, maxImageSize, plugin) {
100
+ try {
101
+ switch (typeof item) {
102
+ case "string":
103
+ return item ? { type: "text", text: item } : null;
104
+
105
+ case "object":
106
+ switch (item.type) {
107
+ case "text":
108
+ // Handle text content, but also check if it's stringified JSON containing documents
109
+ if (!item.text) return null;
110
+
111
+ // Try to parse stringified JSON to check if it's actually a document
112
+ let parsedText = item.text;
113
+ if (typeof item.text === 'string' && item.text.startsWith('{')) {
114
+ try {
115
+ const parsed = JSON.parse(item.text);
116
+ // If this is stringified JSON for an image_url or document, process it accordingly
117
+ if (parsed.type === 'image_url') {
118
+ return await convertContentItemClaude4(parsed, maxImageSize, plugin);
119
+ } else if (parsed.type === 'document') {
120
+ return await convertContentItemClaude4(parsed, maxImageSize, plugin);
121
+ }
122
+ } catch (e) {
123
+ // Not valid JSON, treat as plain text
124
+ }
125
+ }
126
+
127
+ return { type: "text", text: parsedText };
128
+
129
+ case "tool_use":
130
+ return {
131
+ type: "tool_use",
132
+ id: item.id,
133
+ name: item.name,
134
+ input: typeof item.input === 'string' ? { query: item.input } : item.input
135
+ };
136
+
137
+ case "tool_result":
138
+ return {
139
+ type: "tool_result",
140
+ tool_use_id: item.tool_use_id,
141
+ content: item.content
142
+ };
143
+
144
+ case "image_url":
145
+ // Handle images and documents coming as image_url type
146
+ // May include: image_url.url, url, originalFilename
147
+ // Note: gcs URLs are for Google models only, Claude uses the main url
148
+ // Handle both: { image_url: "string" } and { image_url: { url: "string" } }
149
+ let imageUrl = item.url || item.image_url?.url;
150
+ if (typeof item.image_url === 'string') {
151
+ imageUrl = item.image_url;
152
+ }
153
+
154
+ const originalFilename = item.originalFilename || '';
155
+
156
+ if (!imageUrl) {
157
+ logger.warn("Could not parse image URL from content - skipping image content.");
158
+ return null;
159
+ }
160
+
161
+ // Check if this is actually a PDF document (by filename or URL extension)
162
+ // Do this BEFORE image validation since PDFs are not images
163
+ const isPDF = originalFilename.toLowerCase().endsWith('.pdf') ||
164
+ detectFileType(imageUrl) === 'pdf';
165
+ const isTxt = originalFilename.toLowerCase().endsWith('.txt') ||
166
+ originalFilename.toLowerCase().endsWith('.md');
167
+
168
+ // Handle PDF documents
169
+ if (isPDF) {
170
+ try {
171
+ // Fetch the PDF from the URL (Azure Blob Storage or http/https)
172
+ const pdfData = await fetchFileAsDataURL(imageUrl, 'pdf');
173
+ const base64Pdf = pdfData.split(",")[1];
174
+ const pdfSize = Buffer.from(base64Pdf, 'base64').length;
175
+
176
+ if (pdfSize > maxImageSize) {
177
+ logger.warn(`PDF size ${pdfSize} bytes exceeds maximum allowed size ${maxImageSize} - skipping PDF content.`);
178
+ return null;
179
+ }
180
+
181
+ return {
182
+ type: "document",
183
+ source: {
184
+ type: "base64",
185
+ media_type: "application/pdf",
186
+ data: base64Pdf
187
+ }
188
+ };
189
+ } catch (error) {
190
+ logger.error(`Failed to fetch PDF from image_url field: ${error.message}`);
191
+ return null;
192
+ }
193
+ }
194
+
195
+ // Handle text documents
196
+ if (isTxt) {
197
+ try {
198
+ const textContent = await fetchTextFileAsString(imageUrl);
199
+ return {
200
+ type: "text",
201
+ text: textContent
202
+ };
203
+ } catch (error) {
204
+ logger.error(`Failed to fetch text file from image_url field: ${error.message}`);
205
+ return null;
206
+ }
207
+ }
208
+
209
+ // Only validate and handle as image if not a document
210
+ try {
211
+ if (!await plugin.validateImageUrl(imageUrl)) {
212
+ return null;
213
+ }
214
+
215
+ const urlData = imageUrl.startsWith("data:") ? imageUrl : await fetchImageAsDataURL(imageUrl);
216
+ if (!urlData) { return null; }
217
+
218
+ const base64Image = urlData.split(",")[1];
219
+ const base64Size = Buffer.from(base64Image, 'base64').length;
220
+
221
+ if (base64Size > maxImageSize) {
222
+ logger.warn(`Image size ${base64Size} bytes exceeds maximum allowed size ${maxImageSize} - skipping image content.`);
223
+ return null;
224
+ }
225
+
226
+ const [, mimeType = "image/jpeg"] = urlData.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+).*,.*/) || [];
227
+
228
+ return {
229
+ type: "image",
230
+ source: {
231
+ type: "base64",
232
+ media_type: mimeType,
233
+ data: base64Image,
234
+ },
235
+ };
236
+ } catch (error) {
237
+ logger.error(`Failed to process image: ${error.message}`);
238
+ return null;
239
+ }
240
+
241
+ case "document":
242
+ // Handle Claude document blocks (PDFs and text files)
243
+ const documentUrl = item.url || item.source?.url;
244
+ const documentData = item.data || item.source?.data;
245
+ const documentFileId = item.file_id || item.source?.file_id;
246
+
247
+ if (documentFileId) {
248
+ // Use file_id reference
249
+ return {
250
+ type: "document",
251
+ source: {
252
+ type: "file",
253
+ file_id: documentFileId
254
+ }
255
+ };
256
+ } else if (documentUrl) {
257
+ // Determine file type
258
+ const fileType = detectFileType(documentUrl);
259
+
260
+ if (fileType === 'pdf') {
261
+ // Fetch PDF and convert to base64
262
+ const pdfData = await fetchFileAsDataURL(documentUrl, 'pdf');
263
+ const base64Pdf = pdfData.split(",")[1];
264
+ const pdfSize = Buffer.from(base64Pdf, 'base64').length;
265
+
266
+ if (pdfSize > maxImageSize) {
267
+ logger.warn(`PDF size ${pdfSize} bytes exceeds maximum allowed size ${maxImageSize} - skipping PDF content.`);
268
+ return null;
269
+ }
270
+
271
+ return {
272
+ type: "document",
273
+ source: {
274
+ type: "base64",
275
+ media_type: "application/pdf",
276
+ data: base64Pdf
277
+ }
278
+ };
279
+ } else if (fileType === 'text') {
280
+ // For text files, we can send as plain text or base64
281
+ // Using plain text is more efficient
282
+ const textContent = await fetchTextFileAsString(documentUrl);
283
+ return {
284
+ type: "text",
285
+ text: textContent
286
+ };
287
+ } else {
288
+ logger.warn(`Unsupported document type for URL: ${documentUrl}`);
289
+ return null;
290
+ }
291
+ } else if (documentData) {
292
+ // Already have base64 data
293
+ const mediaType = item.media_type || item.source?.media_type || "application/pdf";
294
+
295
+ if (mediaType.includes('pdf')) {
296
+ const pdfSize = Buffer.from(documentData, 'base64').length;
297
+
298
+ if (pdfSize > maxImageSize) {
299
+ logger.warn(`PDF size ${pdfSize} bytes exceeds maximum allowed size ${maxImageSize} - skipping PDF content.`);
300
+ return null;
301
+ }
302
+
303
+ return {
304
+ type: "document",
305
+ source: {
306
+ type: "base64",
307
+ media_type: "application/pdf",
308
+ data: documentData
309
+ }
310
+ };
311
+ } else if (mediaType.includes('text')) {
312
+ // Decode base64 text data
313
+ const textContent = Buffer.from(documentData, 'base64').toString('utf-8');
314
+ return {
315
+ type: "text",
316
+ text: textContent
317
+ };
318
+ }
319
+ }
320
+
321
+ logger.warn("Could not parse document content - skipping document.");
322
+ return null;
323
+
324
+ default:
325
+ return null;
326
+ }
327
+
328
+ default:
329
+ return null;
330
+ }
331
+ } catch (e) {
332
+ logger.warn(`Error converting content item: ${e}`);
333
+ return null;
334
+ }
335
+ }
336
+
337
+ class Claude4VertexPlugin extends Claude3VertexPlugin {
338
+
339
+ constructor(pathway, model) {
340
+ super(pathway, model);
341
+ this.isMultiModal = true;
342
+ }
343
+
344
+ // Override to use 30MB default for Claude 4 (instead of 20MB)
345
+ getModelMaxImageSize() {
346
+ return (this.promptParameters.maxImageSize ?? this.model.maxImageSize ?? CLAUDE4_DEFAULT_MAX_FILE_SIZE);
347
+ }
348
+
349
+ // Override convertMessagesToClaudeVertex to use the extended content conversion
350
+ async convertMessagesToClaudeVertex(messages) {
351
+ // Create a deep copy of the input messages
352
+ const messagesCopy = JSON.parse(JSON.stringify(messages));
353
+
354
+ let system = "";
355
+ let imageCount = 0;
356
+ const maxImages = 20; // Claude allows up to 20 images per request
357
+
358
+ // Extract system messages
359
+ const systemMessages = messagesCopy.filter(message => message.role === "system");
360
+ if (systemMessages.length > 0) {
361
+ system = systemMessages.map(message => {
362
+ if (Array.isArray(message.content)) {
363
+ return message.content
364
+ .filter(item => item.type === 'text')
365
+ .map(item => item.text)
366
+ .join("\n");
367
+ }
368
+ return message.content;
369
+ }).join("\n");
370
+ }
371
+
372
+ // Filter out system messages and empty messages
373
+ let modifiedMessages = messagesCopy
374
+ .filter(message => message.role !== "system")
375
+ .map(message => {
376
+ if (message.tool_calls) {
377
+ return {
378
+ role: message.role,
379
+ content: message.tool_calls.map(toolCall => ({
380
+ type: "tool_use",
381
+ id: toolCall.id,
382
+ name: toolCall.function.name,
383
+ input: JSON.parse(toolCall.function.arguments)
384
+ }))
385
+ };
386
+ }
387
+
388
+ if (message.role === "tool") {
389
+ return {
390
+ role: "user",
391
+ content: [{
392
+ type: "tool_result",
393
+ tool_use_id: message.tool_call_id,
394
+ content: message.content
395
+ }]
396
+ };
397
+ }
398
+
399
+ return { ...message };
400
+ })
401
+ .filter(message => {
402
+ if (!message.content) return false;
403
+ if (Array.isArray(message.content) && message.content.length === 0) return false;
404
+ return true;
405
+ });
406
+
407
+ // Combine consecutive messages from the same author
408
+ const combinedMessages = modifiedMessages.reduce((acc, message) => {
409
+ if (acc.length === 0 || message.role !== acc[acc.length - 1].role) {
410
+ acc.push({ ...message });
411
+ } else {
412
+ const lastMessage = acc[acc.length - 1];
413
+ if (Array.isArray(lastMessage.content) && Array.isArray(message.content)) {
414
+ lastMessage.content = [...lastMessage.content, ...message.content];
415
+ } else if (Array.isArray(lastMessage.content)) {
416
+ lastMessage.content.push({ type: 'text', text: message.content });
417
+ } else if (Array.isArray(message.content)) {
418
+ lastMessage.content = [{ type: 'text', text: lastMessage.content }, ...message.content];
419
+ } else {
420
+ lastMessage.content += "\n" + message.content;
421
+ }
422
+ }
423
+ return acc;
424
+ }, []);
425
+
426
+ // Ensure an odd number of messages
427
+ const finalMessages = combinedMessages.length % 2 === 0
428
+ ? combinedMessages.slice(1)
429
+ : combinedMessages;
430
+
431
+ // Convert content items using the extended conversion function
432
+ const claude4Messages = await Promise.all(
433
+ finalMessages.map(async (message) => {
434
+ const contentArray = Array.isArray(message.content) ? message.content : [message.content];
435
+ const claude4Content = await Promise.all(contentArray.map(async item => {
436
+ const convertedItem = await convertContentItemClaude4(item, this.getModelMaxImageSize(), this);
437
+
438
+ // Track image count
439
+ if (convertedItem?.type === 'image') {
440
+ imageCount++;
441
+ if (imageCount > maxImages) {
442
+ logger.warn(`Maximum number of images (${maxImages}) exceeded - skipping additional images.`);
443
+ return null;
444
+ }
445
+ }
446
+
447
+ return convertedItem;
448
+ }));
449
+ return {
450
+ role: message.role,
451
+ content: claude4Content.filter(Boolean),
452
+ };
453
+ })
454
+ );
455
+
456
+ return {
457
+ system,
458
+ modifiedMessages: claude4Messages,
459
+ };
460
+ }
461
+
462
+ // Override logging to handle document blocks
463
+ logRequestData(data, responseData, prompt) {
464
+ const { stream, messages, system } = data;
465
+ if (system) {
466
+ const { length, units } = this.getLength(system);
467
+ logger.info(`[system messages sent containing ${length} ${units}]`);
468
+ logger.verbose(`${this.shortenContent(system)}`);
469
+ }
470
+
471
+ if (messages && messages.length > 1) {
472
+ logger.info(`[chat request sent containing ${messages.length} messages]`);
473
+ let totalLength = 0;
474
+ let totalUnits;
475
+ messages.forEach((message, index) => {
476
+ const content = Array.isArray(message.content)
477
+ ? message.content.map((item) => {
478
+ if (item.source && item.source.type === 'base64') {
479
+ item.source.data = '* base64 data truncated for log *';
480
+ }
481
+ if (item.type === 'document') {
482
+ return `{type: document, source: ${JSON.stringify(item.source)}}`;
483
+ }
484
+ return JSON.stringify(item);
485
+ }).join(", ")
486
+ : message.content;
487
+ const { length, units } = this.getLength(content);
488
+ const preview = this.shortenContent(content);
489
+
490
+ logger.verbose(
491
+ `message ${index + 1}: role: ${
492
+ message.role
493
+ }, ${units}: ${length}, content: "${preview}"`
494
+ );
495
+ totalLength += length;
496
+ totalUnits = units;
497
+ });
498
+ logger.info(`[chat request contained ${totalLength} ${totalUnits}]`);
499
+ } else {
500
+ const message = messages[0];
501
+ const content = Array.isArray(message.content)
502
+ ? message.content.map((item) => {
503
+ if (item.source && item.source.type === 'base64') {
504
+ item.source.data = '* base64 data truncated for log *';
505
+ }
506
+ if (item.type === 'document') {
507
+ return `{type: document, source: ${JSON.stringify(item.source)}}`;
508
+ }
509
+ return JSON.stringify(item);
510
+ }).join(", ")
511
+ : message.content;
512
+ const { length, units } = this.getLength(content);
513
+ logger.info(`[request sent containing ${length} ${units}]`);
514
+ logger.verbose(`${this.shortenContent(content)}`);
515
+ }
516
+
517
+ if (stream) {
518
+ logger.info(`[response received as an SSE stream]`);
519
+ } else {
520
+ const parsedResponse = this.parseResponse(responseData);
521
+
522
+ if (typeof parsedResponse === 'string') {
523
+ const { length, units } = this.getLength(parsedResponse);
524
+ logger.info(`[response received containing ${length} ${units}]`);
525
+ logger.verbose(`${this.shortenContent(parsedResponse)}`);
526
+ } else {
527
+ logger.info(`[response received containing object]`);
528
+ logger.verbose(`${JSON.stringify(parsedResponse)}`);
529
+ }
530
+ }
531
+
532
+ prompt &&
533
+ prompt.debugInfo &&
534
+ (prompt.debugInfo += `\n${JSON.stringify(data)}`);
535
+ }
536
+
537
+ }
538
+
539
+ export default Claude4VertexPlugin;
540
+