@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
|
@@ -16,6 +16,42 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
16
16
|
super(pathway, model);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Minimal 429 retry wrapper for Whisper API calls
|
|
20
|
+
async executeWhisperRequest(cortexRequest) {
|
|
21
|
+
const maxRetries = 9;
|
|
22
|
+
|
|
23
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
24
|
+
try {
|
|
25
|
+
return await this.executeRequest(cortexRequest);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
|
|
28
|
+
// Check if it's a 429 error
|
|
29
|
+
const is429 = error?.status === 429 ||
|
|
30
|
+
error?.response?.status === 429 ||
|
|
31
|
+
error?.message?.includes('429');
|
|
32
|
+
|
|
33
|
+
if (!is429 || attempt === maxRetries - 1) {
|
|
34
|
+
// Not a 429 or max retries reached, rethrow
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Calculate backoff delay (exponential with jitter)
|
|
39
|
+
const retryAfter = error?.response?.headers?.['retry-after'];
|
|
40
|
+
// Fix: Validate parseInt result to prevent NaN
|
|
41
|
+
const baseDelay = retryAfter && !isNaN(parseInt(retryAfter))
|
|
42
|
+
? parseInt(retryAfter) * 1000
|
|
43
|
+
: 2000 * Math.pow(2, attempt);
|
|
44
|
+
const jitter = baseDelay * 0.2 * Math.random();
|
|
45
|
+
const delay = baseDelay + jitter;
|
|
46
|
+
|
|
47
|
+
logger.warn(`Whisper 429 error (attempt ${attempt + 1}/${maxRetries}). Retrying in ${Math.round(delay)}ms`);
|
|
48
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Remove unreachable code - this line was never reached
|
|
53
|
+
}
|
|
54
|
+
|
|
19
55
|
// Execute the request to the OpenAI Whisper API
|
|
20
56
|
async execute(text, parameters, prompt, cortexRequest) {
|
|
21
57
|
const { pathwayResolver } = cortexRequest;
|
|
@@ -47,7 +83,9 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
47
83
|
};
|
|
48
84
|
|
|
49
85
|
cortexRequest.initCallback = whisperInitCallback;
|
|
50
|
-
|
|
86
|
+
|
|
87
|
+
// return this.executeRequest(cortexRequest);
|
|
88
|
+
return this.executeWhisperRequest(cortexRequest);
|
|
51
89
|
|
|
52
90
|
} catch (err) {
|
|
53
91
|
logger.error(`Error getting word timestamped data from api: ${err}`);
|
|
@@ -72,7 +110,10 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
72
110
|
cortexRequest.initCallback = whisperInitCallback;
|
|
73
111
|
|
|
74
112
|
sendProgress(true, true);
|
|
75
|
-
|
|
113
|
+
|
|
114
|
+
// const res = await this.executeRequest(cortexRequest);
|
|
115
|
+
const res = await this.executeWhisperRequest(cortexRequest);
|
|
116
|
+
|
|
76
117
|
if (!res) {
|
|
77
118
|
throw new Error('Received null or empty response');
|
|
78
119
|
}
|
|
@@ -44,4 +44,125 @@ test('Claude SSE chat stream returns OAI-style chunks', async (t) => {
|
|
|
44
44
|
t.true(assertAnyContentDelta(chunks));
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
test('Claude 4 SSE chat stream with document block (PDF)', async (t) => {
|
|
48
|
+
const baseUrl = `http://localhost:${process.env.CORTEX_PORT}/v1`;
|
|
49
|
+
|
|
50
|
+
// Pick an available Claude 4 model from /models
|
|
51
|
+
let model = null;
|
|
52
|
+
try {
|
|
53
|
+
const res = await got(`${baseUrl}/models`, { responseType: 'json' });
|
|
54
|
+
const ids = (res.body?.data || []).map(m => m.id);
|
|
55
|
+
// Look for claude-4 or claude-45 models specifically
|
|
56
|
+
model = ids.find(id => /^claude-(4|45)/.test(id));
|
|
57
|
+
} catch (_) {}
|
|
58
|
+
|
|
59
|
+
if (!model) {
|
|
60
|
+
t.pass('Skipping - no Claude 4+ model configured');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create a simple PDF document with base64 encoding (sample dummy PDF)
|
|
65
|
+
// This is a minimal valid PDF
|
|
66
|
+
const pdfContent = '%PDF-1.4\n%âãÏÓ\n1 0 obj\n<</Type/Catalog/Pages 2 0 R>>\nendobj\n2 0 obj\n<</Type/Pages/Kids[3 0 R]/Count 1>>\nendobj\n3 0 obj\n<</Type/Page/Parent 2 0 R/Resources<</Font<</F1 4 0 R>>>>/MediaBox[0 0 612 792]/Contents 5 0 R>>\nendobj\n4 0 obj\n<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>\nendobj\n5 0 obj\n<</Length 44>>\nstream\nBT\n/F1 12 Tf\n100 700 Td\n(Sample PDF) Tj\nET\nendstream\nendobj\nxref\n0 6\n0000000000 65535 f\n0000000010 00000 n\n0000000053 00000 n\n0000000102 00000 n\n0000000211 00000 n\n0000000280 00000 n\ntrailer\n<</Size 6/Root 1 0 R>>\nstartxref\n369\n%%EOF';
|
|
67
|
+
const base64Pdf = Buffer.from(pdfContent).toString('base64');
|
|
68
|
+
|
|
69
|
+
const payload = {
|
|
70
|
+
model,
|
|
71
|
+
messages: [
|
|
72
|
+
{
|
|
73
|
+
role: 'user',
|
|
74
|
+
content: [
|
|
75
|
+
{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: 'Please analyze this PDF document and tell me what you see. Be concise.'
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'document',
|
|
81
|
+
source: {
|
|
82
|
+
type: 'base64',
|
|
83
|
+
media_type: 'application/pdf',
|
|
84
|
+
data: base64Pdf
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
stream: true,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const chunks = await collectSSEChunks(baseUrl, '/chat/completions', payload);
|
|
95
|
+
t.true(chunks.length > 0, 'Should receive SSE chunks');
|
|
96
|
+
chunks.forEach(ch => assertOAIChatChunkBasics(t, ch));
|
|
97
|
+
t.true(assertAnyContentDelta(chunks), 'Should have content delta in chunks');
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// If the model doesn't support this format yet, skip gracefully
|
|
100
|
+
if (err.message && err.message.includes('document')) {
|
|
101
|
+
t.pass('Document blocks not yet supported by this model endpoint');
|
|
102
|
+
} else {
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('Claude 4 SSE chat stream with text document', async (t) => {
|
|
109
|
+
const baseUrl = `http://localhost:${process.env.CORTEX_PORT}/v1`;
|
|
110
|
+
|
|
111
|
+
// Pick an available Claude 4 model from /models
|
|
112
|
+
let model = null;
|
|
113
|
+
try {
|
|
114
|
+
const res = await got(`${baseUrl}/models`, { responseType: 'json' });
|
|
115
|
+
const ids = (res.body?.data || []).map(m => m.id);
|
|
116
|
+
// Look for claude-4 or claude-45 models specifically
|
|
117
|
+
model = ids.find(id => /^claude-(4|45)/.test(id));
|
|
118
|
+
} catch (_) {}
|
|
119
|
+
|
|
120
|
+
if (!model) {
|
|
121
|
+
t.pass('Skipping - no Claude 4+ model configured');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create a simple text document with base64 encoding
|
|
126
|
+
const textContent = 'This is a sample text document.\nIt contains multiple lines.\nThe document discusses the capabilities of Claude models with document support.';
|
|
127
|
+
const base64Text = Buffer.from(textContent).toString('base64');
|
|
128
|
+
|
|
129
|
+
const payload = {
|
|
130
|
+
model,
|
|
131
|
+
messages: [
|
|
132
|
+
{
|
|
133
|
+
role: 'user',
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: 'text',
|
|
137
|
+
text: 'Please summarize this text document for me in one sentence.'
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'document',
|
|
141
|
+
source: {
|
|
142
|
+
type: 'base64',
|
|
143
|
+
media_type: 'text/plain',
|
|
144
|
+
data: base64Text
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
stream: true,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const chunks = await collectSSEChunks(baseUrl, '/chat/completions', payload);
|
|
155
|
+
t.true(chunks.length > 0, 'Should receive SSE chunks');
|
|
156
|
+
chunks.forEach(ch => assertOAIChatChunkBasics(t, ch));
|
|
157
|
+
t.true(assertAnyContentDelta(chunks), 'Should have content delta in chunks');
|
|
158
|
+
} catch (err) {
|
|
159
|
+
// If the model doesn't support this format yet, skip gracefully
|
|
160
|
+
if (err.message && err.message.includes('document')) {
|
|
161
|
+
t.pass('Document blocks not yet supported by this model endpoint');
|
|
162
|
+
} else {
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
47
168
|
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import Claude4VertexPlugin from '../../../server/plugins/claude4VertexPlugin.js';
|
|
3
|
+
import { mockPathwayResolverMessages } from '../../helpers/mocks.js';
|
|
4
|
+
import { config } from '../../../config.js';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
// Helper function to load test data from files
|
|
9
|
+
function loadTestData(filename) {
|
|
10
|
+
try {
|
|
11
|
+
const filePath = path.join(process.cwd(), 'tests', 'data', filename);
|
|
12
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
13
|
+
} catch (error) {
|
|
14
|
+
// Throw error to make test failures explicit when required data is missing
|
|
15
|
+
throw new Error(`Failed to load required test data file ${filename}: ${error.message}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { pathway, model } = mockPathwayResolverMessages;
|
|
20
|
+
|
|
21
|
+
test('constructor', (t) => {
|
|
22
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
23
|
+
t.is(plugin.config, config);
|
|
24
|
+
t.is(plugin.pathwayPrompt, mockPathwayResolverMessages.pathway.prompt);
|
|
25
|
+
t.true(plugin.isMultiModal);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('parseResponse', (t) => {
|
|
29
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
30
|
+
|
|
31
|
+
// Test text content response
|
|
32
|
+
const dataWithTextContent = {
|
|
33
|
+
content: [
|
|
34
|
+
{ type: 'text', text: 'Hello, World!' }
|
|
35
|
+
],
|
|
36
|
+
usage: { input_tokens: 10, output_tokens: 5 },
|
|
37
|
+
stop_reason: 'end_turn'
|
|
38
|
+
};
|
|
39
|
+
const resultWithTextContent = plugin.parseResponse(dataWithTextContent);
|
|
40
|
+
t.truthy(resultWithTextContent.output_text === 'Hello, World!');
|
|
41
|
+
t.truthy(resultWithTextContent.finishReason === 'stop');
|
|
42
|
+
t.truthy(resultWithTextContent.usage);
|
|
43
|
+
t.truthy(resultWithTextContent.metadata.model === plugin.modelName);
|
|
44
|
+
|
|
45
|
+
// Test tool calls response
|
|
46
|
+
const dataWithToolCalls = {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: 'tool_use',
|
|
50
|
+
id: 'tool_1',
|
|
51
|
+
name: 'search_web',
|
|
52
|
+
input: { query: 'test search' }
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
usage: { input_tokens: 15, output_tokens: 8 },
|
|
56
|
+
stop_reason: 'tool_use'
|
|
57
|
+
};
|
|
58
|
+
const resultWithToolCalls = plugin.parseResponse(dataWithToolCalls);
|
|
59
|
+
t.truthy(resultWithToolCalls.output_text === '');
|
|
60
|
+
t.truthy(resultWithToolCalls.finishReason === 'tool_calls');
|
|
61
|
+
t.truthy(resultWithToolCalls.toolCalls);
|
|
62
|
+
t.truthy(resultWithToolCalls.toolCalls.length === 1);
|
|
63
|
+
t.truthy(resultWithToolCalls.toolCalls[0].id === 'tool_1');
|
|
64
|
+
t.truthy(resultWithToolCalls.toolCalls[0].function.name === 'search_web');
|
|
65
|
+
t.truthy(resultWithToolCalls.toolCalls[0].function.arguments === '{"query":"test search"}');
|
|
66
|
+
|
|
67
|
+
// Test data without content (should return original data)
|
|
68
|
+
const dataWithoutContent = {};
|
|
69
|
+
const resultWithoutContent = plugin.parseResponse(dataWithoutContent);
|
|
70
|
+
t.deepEqual(resultWithoutContent, dataWithoutContent);
|
|
71
|
+
|
|
72
|
+
// Test null data (should return null)
|
|
73
|
+
const dataNull = null;
|
|
74
|
+
const resultNull = plugin.parseResponse(dataNull);
|
|
75
|
+
t.is(resultNull, dataNull);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('convertMessagesToClaudeVertex text message', async (t) => {
|
|
79
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
80
|
+
// Test with text message
|
|
81
|
+
let messages = [
|
|
82
|
+
{ role: 'system', content: 'System message' },
|
|
83
|
+
{ role: 'user', content: 'User message' },
|
|
84
|
+
{ role: 'assistant', content: 'Assistant message' },
|
|
85
|
+
{ role: 'user', content: 'User message 2' },
|
|
86
|
+
];
|
|
87
|
+
let output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
88
|
+
t.deepEqual(output, {
|
|
89
|
+
system: 'System message',
|
|
90
|
+
modifiedMessages: [
|
|
91
|
+
{
|
|
92
|
+
role: "user",
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: "text",
|
|
96
|
+
text: "User message",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
role: "assistant",
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: "Assistant message",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
role: "user",
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: "User message 2",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('convertMessagesToClaudeVertex image_url message', async (t) => {
|
|
123
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
124
|
+
// Test with image_url message
|
|
125
|
+
const messages = [
|
|
126
|
+
{
|
|
127
|
+
role: 'assistant',
|
|
128
|
+
content: {
|
|
129
|
+
type: 'image_url',
|
|
130
|
+
image_url: 'https://static.toiimg.com/thumb/msid-102827471,width-1280,height-720,resizemode-4/102827471.jpg'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
135
|
+
|
|
136
|
+
// Define a regex for base64 validation
|
|
137
|
+
const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
138
|
+
const base64Data = output.modifiedMessages[0].content[0].source.data;
|
|
139
|
+
|
|
140
|
+
t.is(output.system, '');
|
|
141
|
+
t.is(output.modifiedMessages[0].role, 'assistant');
|
|
142
|
+
t.is(output.modifiedMessages[0].content[0].type, 'image');
|
|
143
|
+
t.is(output.modifiedMessages[0].content[0].source.type, 'base64');
|
|
144
|
+
t.is(output.modifiedMessages[0].content[0].source.media_type, 'image/jpeg');
|
|
145
|
+
|
|
146
|
+
// Check if the base64 data looks reasonable
|
|
147
|
+
t.true(base64Data.length > 100); // Check if the data is sufficiently long
|
|
148
|
+
t.true(base64Regex.test(base64Data)); // Check if the data matches the base64 regex
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('convertMessagesToClaudeVertex stringified JSON PDF (real-world format)', async (t) => {
|
|
152
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
153
|
+
|
|
154
|
+
// Test with stringified JSON PDF content (how it actually comes from the system)
|
|
155
|
+
const base64Pdf = Buffer.from('Sample PDF content').toString('base64');
|
|
156
|
+
const stringifiedPdf = JSON.stringify({
|
|
157
|
+
type: 'image_url',
|
|
158
|
+
url: 'data:application/pdf;base64,' + base64Pdf,
|
|
159
|
+
image_url: { url: 'data:application/pdf;base64,' + base64Pdf },
|
|
160
|
+
originalFilename: 'Invoice.pdf'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const messages = [
|
|
164
|
+
{
|
|
165
|
+
role: 'user',
|
|
166
|
+
content: [
|
|
167
|
+
{
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: 'Please analyze this document'
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
type: 'text',
|
|
173
|
+
text: stringifiedPdf // Content item is stringified JSON
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
180
|
+
|
|
181
|
+
// Should have 2 content items: text + document
|
|
182
|
+
t.is(output.modifiedMessages[0].role, 'user');
|
|
183
|
+
t.is(output.modifiedMessages[0].content.length, 2);
|
|
184
|
+
|
|
185
|
+
// First should be text
|
|
186
|
+
t.is(output.modifiedMessages[0].content[0].type, 'text');
|
|
187
|
+
t.is(output.modifiedMessages[0].content[0].text, 'Please analyze this document');
|
|
188
|
+
|
|
189
|
+
// Second should be converted to document block (not text!)
|
|
190
|
+
t.is(output.modifiedMessages[0].content[1].type, 'document');
|
|
191
|
+
t.is(output.modifiedMessages[0].content[1].source.type, 'base64');
|
|
192
|
+
t.is(output.modifiedMessages[0].content[1].source.media_type, 'application/pdf');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('convertMessagesToClaudeVertex document block with PDF URL', async (t) => {
|
|
196
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
197
|
+
|
|
198
|
+
// Test with document block containing PDF URL
|
|
199
|
+
const messages = [
|
|
200
|
+
{
|
|
201
|
+
role: 'user',
|
|
202
|
+
content: [
|
|
203
|
+
{
|
|
204
|
+
type: 'document',
|
|
205
|
+
source: {
|
|
206
|
+
type: 'url',
|
|
207
|
+
url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
215
|
+
|
|
216
|
+
// Verify the document was converted
|
|
217
|
+
t.is(output.modifiedMessages[0].role, 'user');
|
|
218
|
+
t.is(output.modifiedMessages[0].content[0].type, 'document');
|
|
219
|
+
t.is(output.modifiedMessages[0].content[0].source.type, 'base64');
|
|
220
|
+
t.is(output.modifiedMessages[0].content[0].source.media_type, 'application/pdf');
|
|
221
|
+
|
|
222
|
+
// Verify base64 data exists
|
|
223
|
+
const base64Data = output.modifiedMessages[0].content[0].source.data;
|
|
224
|
+
const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
225
|
+
t.true(base64Data.length > 100);
|
|
226
|
+
t.true(base64Regex.test(base64Data));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('convertMessagesToClaudeVertex document block with text file URL', async (t) => {
|
|
230
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
231
|
+
|
|
232
|
+
// Test with document block containing text file data (base64)
|
|
233
|
+
const textContent = 'Sample text file content';
|
|
234
|
+
const base64Text = Buffer.from(textContent).toString('base64');
|
|
235
|
+
|
|
236
|
+
const messages = [
|
|
237
|
+
{
|
|
238
|
+
role: 'user',
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type: 'document',
|
|
242
|
+
source: {
|
|
243
|
+
type: 'base64',
|
|
244
|
+
media_type: 'text/plain',
|
|
245
|
+
data: base64Text
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
253
|
+
|
|
254
|
+
// For text files, should be converted to text content block
|
|
255
|
+
t.is(output.modifiedMessages[0].role, 'user');
|
|
256
|
+
t.is(output.modifiedMessages[0].content[0].type, 'text');
|
|
257
|
+
t.is(output.modifiedMessages[0].content[0].text, textContent);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('convertMessagesToClaudeVertex document block with file_id', async (t) => {
|
|
261
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
262
|
+
|
|
263
|
+
// Test with document block containing file_id
|
|
264
|
+
const messages = [
|
|
265
|
+
{
|
|
266
|
+
role: 'user',
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: 'document',
|
|
270
|
+
source: {
|
|
271
|
+
type: 'file',
|
|
272
|
+
file_id: 'file_abc123'
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
280
|
+
|
|
281
|
+
// Should pass through file_id reference
|
|
282
|
+
t.is(output.modifiedMessages[0].role, 'user');
|
|
283
|
+
t.is(output.modifiedMessages[0].content[0].type, 'document');
|
|
284
|
+
t.is(output.modifiedMessages[0].content[0].source.type, 'file');
|
|
285
|
+
t.is(output.modifiedMessages[0].content[0].source.file_id, 'file_abc123');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('convertMessagesToClaudeVertex document block with base64 PDF', async (t) => {
|
|
289
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
290
|
+
|
|
291
|
+
// Create a sample base64 PDF string
|
|
292
|
+
const base64Pdf = Buffer.from('Sample PDF content').toString('base64');
|
|
293
|
+
|
|
294
|
+
const messages = [
|
|
295
|
+
{
|
|
296
|
+
role: 'user',
|
|
297
|
+
content: [
|
|
298
|
+
{
|
|
299
|
+
type: 'document',
|
|
300
|
+
source: {
|
|
301
|
+
type: 'base64',
|
|
302
|
+
media_type: 'application/pdf',
|
|
303
|
+
data: base64Pdf
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
311
|
+
|
|
312
|
+
// Should pass through as document block
|
|
313
|
+
t.is(output.modifiedMessages[0].role, 'user');
|
|
314
|
+
t.is(output.modifiedMessages[0].content[0].type, 'document');
|
|
315
|
+
t.is(output.modifiedMessages[0].content[0].source.type, 'base64');
|
|
316
|
+
t.is(output.modifiedMessages[0].content[0].source.media_type, 'application/pdf');
|
|
317
|
+
t.is(output.modifiedMessages[0].content[0].source.data, base64Pdf);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('convertMessagesToClaudeVertex mixed content with documents', async (t) => {
|
|
321
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
322
|
+
|
|
323
|
+
// Test with mixed content including document and text
|
|
324
|
+
const base64Pdf = Buffer.from('Sample PDF content').toString('base64');
|
|
325
|
+
|
|
326
|
+
const messages = [
|
|
327
|
+
{
|
|
328
|
+
role: 'user',
|
|
329
|
+
content: [
|
|
330
|
+
{
|
|
331
|
+
type: 'text',
|
|
332
|
+
text: 'Please analyze this document'
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
type: 'document',
|
|
336
|
+
source: {
|
|
337
|
+
type: 'base64',
|
|
338
|
+
media_type: 'application/pdf',
|
|
339
|
+
data: base64Pdf
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
347
|
+
|
|
348
|
+
// Should have both text and document blocks
|
|
349
|
+
t.is(output.modifiedMessages[0].role, 'user');
|
|
350
|
+
t.is(output.modifiedMessages[0].content.length, 2);
|
|
351
|
+
t.is(output.modifiedMessages[0].content[0].type, 'text');
|
|
352
|
+
t.is(output.modifiedMessages[0].content[1].type, 'document');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('convertMessagesToClaudeVertex unsupported type', async (t) => {
|
|
356
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
357
|
+
// Test with unsupported type
|
|
358
|
+
const messages = [{ role: 'user', content: { type: 'unsupported_type' } }];
|
|
359
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
360
|
+
t.deepEqual(output, { system: '', modifiedMessages: [{role: 'user', content: [] }] });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('convertMessagesToClaudeVertex empty messages', async (t) => {
|
|
364
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
365
|
+
// Test with empty messages
|
|
366
|
+
const messages = [];
|
|
367
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
368
|
+
t.deepEqual(output, { system: '', modifiedMessages: [] });
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('convertMessagesToClaudeVertex system message', async (t) => {
|
|
372
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
373
|
+
// Test with system message
|
|
374
|
+
const messages = [{ role: 'system', content: 'System message' }];
|
|
375
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
376
|
+
t.deepEqual(output, { system: 'System message', modifiedMessages: [] });
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('convertMessagesToClaudeVertex system message with user message', async (t) => {
|
|
380
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
381
|
+
// Test with system message followed by user message
|
|
382
|
+
const messages = [
|
|
383
|
+
{ role: 'system', content: 'System message' },
|
|
384
|
+
{ role: 'user', content: 'User message' }
|
|
385
|
+
];
|
|
386
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
387
|
+
t.deepEqual(output, {
|
|
388
|
+
system: 'System message',
|
|
389
|
+
modifiedMessages: [{ role: 'user', content: [{ type: 'text', text: 'User message' }] }]
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('convertMessagesToClaudeVertex with multi-part content array', async (t) => {
|
|
394
|
+
const plugin = new Claude4VertexPlugin(pathway, model);
|
|
395
|
+
|
|
396
|
+
// Test with multi-part content array including text, image, and document
|
|
397
|
+
const base64Pdf = Buffer.from('Sample PDF content').toString('base64');
|
|
398
|
+
|
|
399
|
+
const multiPartContent = [
|
|
400
|
+
{
|
|
401
|
+
type: 'text',
|
|
402
|
+
text: 'Hello world'
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
type: 'text',
|
|
406
|
+
text: 'Hello2 world2'
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
type: 'image_url',
|
|
410
|
+
image_url: 'https://static.toiimg.com/thumb/msid-102827471,width-1280,height-720,resizemode-4/102827471.jpg'
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
type: 'document',
|
|
414
|
+
source: {
|
|
415
|
+
type: 'base64',
|
|
416
|
+
media_type: 'application/pdf',
|
|
417
|
+
data: base64Pdf
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
const messages = [
|
|
423
|
+
{ role: 'system', content: 'System message' },
|
|
424
|
+
{ role: 'user', content: multiPartContent }
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
const output = await plugin.convertMessagesToClaudeVertex(messages);
|
|
428
|
+
|
|
429
|
+
// Verify system message is preserved
|
|
430
|
+
t.is(output.system, 'System message');
|
|
431
|
+
|
|
432
|
+
// Verify the user message role is preserved
|
|
433
|
+
t.is(output.modifiedMessages[0].role, 'user');
|
|
434
|
+
|
|
435
|
+
// Verify the content array has the correct number of items
|
|
436
|
+
// We expect 4 items: 2 text items, 1 image item, and 1 document item
|
|
437
|
+
t.is(output.modifiedMessages[0].content.length, 4);
|
|
438
|
+
|
|
439
|
+
// Verify the text content items
|
|
440
|
+
t.is(output.modifiedMessages[0].content[0].type, 'text');
|
|
441
|
+
t.is(output.modifiedMessages[0].content[0].text, 'Hello world');
|
|
442
|
+
|
|
443
|
+
t.is(output.modifiedMessages[0].content[1].type, 'text');
|
|
444
|
+
t.is(output.modifiedMessages[0].content[1].text, 'Hello2 world2');
|
|
445
|
+
|
|
446
|
+
// Verify the image content item
|
|
447
|
+
t.is(output.modifiedMessages[0].content[2].type, 'image');
|
|
448
|
+
t.is(output.modifiedMessages[0].content[2].source.type, 'base64');
|
|
449
|
+
t.is(output.modifiedMessages[0].content[2].source.media_type, 'image/jpeg');
|
|
450
|
+
|
|
451
|
+
// Verify the document content item
|
|
452
|
+
t.is(output.modifiedMessages[0].content[3].type, 'document');
|
|
453
|
+
t.is(output.modifiedMessages[0].content[3].source.type, 'base64');
|
|
454
|
+
t.is(output.modifiedMessages[0].content[3].source.media_type, 'application/pdf');
|
|
455
|
+
|
|
456
|
+
// Check if the base64 data looks reasonable
|
|
457
|
+
const base64Data = output.modifiedMessages[0].content[2].source.data;
|
|
458
|
+
const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
459
|
+
t.true(base64Data.length > 100); // Check if the data is sufficiently long
|
|
460
|
+
t.true(base64Regex.test(base64Data)); // Check if the data matches the base64 regex
|
|
461
|
+
});
|
|
462
|
+
|