@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.
- package/README.md +1 -0
- package/config.js +1 -1
- package/helper-apps/cortex-autogen2/.dockerignore +1 -0
- package/helper-apps/cortex-autogen2/Dockerfile +6 -10
- package/helper-apps/cortex-autogen2/Dockerfile.worker +2 -0
- package/helper-apps/cortex-autogen2/agents.py +203 -2
- package/helper-apps/cortex-autogen2/main.py +1 -1
- package/helper-apps/cortex-autogen2/pyproject.toml +12 -0
- package/helper-apps/cortex-autogen2/requirements.txt +14 -0
- package/helper-apps/cortex-autogen2/services/redis_publisher.py +1 -1
- package/helper-apps/cortex-autogen2/services/run_analyzer.py +1 -1
- package/helper-apps/cortex-autogen2/task_processor.py +431 -229
- package/helper-apps/cortex-autogen2/test_entity_fetcher.py +305 -0
- package/helper-apps/cortex-autogen2/tests/README.md +240 -0
- package/helper-apps/cortex-autogen2/tests/TEST_REPORT.md +342 -0
- package/helper-apps/cortex-autogen2/tests/__init__.py +8 -0
- package/helper-apps/cortex-autogen2/tests/analysis/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/analysis/improvement_suggester.py +224 -0
- package/helper-apps/cortex-autogen2/tests/analysis/trend_analyzer.py +211 -0
- package/helper-apps/cortex-autogen2/tests/cli/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/cli/run_tests.py +296 -0
- package/helper-apps/cortex-autogen2/tests/collectors/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/collectors/log_collector.py +252 -0
- package/helper-apps/cortex-autogen2/tests/collectors/progress_collector.py +182 -0
- package/helper-apps/cortex-autogen2/tests/conftest.py +15 -0
- package/helper-apps/cortex-autogen2/tests/database/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/database/repository.py +501 -0
- package/helper-apps/cortex-autogen2/tests/database/schema.sql +108 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/llm_scorer.py +294 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/prompts.py +250 -0
- package/helper-apps/cortex-autogen2/tests/evaluators/wordcloud_validator.py +168 -0
- package/helper-apps/cortex-autogen2/tests/metrics/__init__.py +1 -0
- package/helper-apps/cortex-autogen2/tests/metrics/collector.py +155 -0
- package/helper-apps/cortex-autogen2/tests/orchestrator.py +576 -0
- package/helper-apps/cortex-autogen2/tests/test_cases.yaml +279 -0
- package/helper-apps/cortex-autogen2/tests/test_data.db +0 -0
- package/helper-apps/cortex-autogen2/tests/utils/__init__.py +3 -0
- package/helper-apps/cortex-autogen2/tests/utils/connectivity.py +112 -0
- package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +74 -24
- package/helper-apps/cortex-autogen2/tools/entity_api_registry.json +38 -0
- package/helper-apps/cortex-autogen2/tools/file_tools.py +1 -1
- package/helper-apps/cortex-autogen2/tools/search_tools.py +436 -238
- package/helper-apps/cortex-file-handler/package-lock.json +2 -2
- package/helper-apps/cortex-file-handler/package.json +1 -1
- package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +4 -5
- package/helper-apps/cortex-file-handler/src/blobHandler.js +36 -144
- package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +5 -3
- package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +34 -1
- package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +22 -0
- package/helper-apps/cortex-file-handler/src/services/storage/LocalStorageProvider.js +28 -1
- package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +29 -4
- package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +11 -0
- package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +1 -1
- package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +3 -2
- package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +8 -1
- package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +5 -2
- package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +14 -7
- package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +5 -2
- package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +31 -19
- package/package.json +1 -1
- package/server/modelExecutor.js +4 -0
- package/server/plugins/claude4VertexPlugin.js +540 -0
- package/server/plugins/openAiWhisperPlugin.js +43 -2
- package/tests/integration/rest/vendors/claude_streaming.test.js +121 -0
- package/tests/unit/plugins/claude4VertexPlugin.test.js +462 -0
- package/tests/unit/plugins/claude4VertexToolConversion.test.js +413 -0
- package/helper-apps/cortex-autogen/.funcignore +0 -8
- package/helper-apps/cortex-autogen/Dockerfile +0 -10
- package/helper-apps/cortex-autogen/OAI_CONFIG_LIST +0 -6
- package/helper-apps/cortex-autogen/agents.py +0 -493
- package/helper-apps/cortex-autogen/agents_extra.py +0 -14
- package/helper-apps/cortex-autogen/config.py +0 -18
- package/helper-apps/cortex-autogen/data_operations.py +0 -29
- package/helper-apps/cortex-autogen/function_app.py +0 -44
- package/helper-apps/cortex-autogen/host.json +0 -15
- package/helper-apps/cortex-autogen/main.py +0 -38
- package/helper-apps/cortex-autogen/prompts.py +0 -196
- package/helper-apps/cortex-autogen/prompts_extra.py +0 -5
- package/helper-apps/cortex-autogen/requirements.txt +0 -9
- package/helper-apps/cortex-autogen/search.py +0 -85
- package/helper-apps/cortex-autogen/test.sh +0 -40
- package/helper-apps/cortex-autogen/tools/sasfileuploader.py +0 -66
- package/helper-apps/cortex-autogen/utils.py +0 -88
- package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +0 -22
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
package/server/modelExecutor.js
CHANGED
|
@@ -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
|
+
|