@aj-archipelago/cortex 1.3.27 → 1.3.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.3.27",
3
+ "version": "1.3.29",
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": {
@@ -2,7 +2,7 @@ import { parse, build } from "@aj-archipelago/subvibe";
2
2
  import logger from "../lib/logger.js";
3
3
  import { callPathway } from "../lib/pathwayTools.js";
4
4
 
5
- function splitIntoOverlappingChunks(captions, chunkSize = 20, overlap = 3) {
5
+ export function splitIntoOverlappingChunks(captions, chunkSize = 20, overlap = 3) {
6
6
  const chunks = [];
7
7
  for (let i = 0; i < captions.length; i += (chunkSize - overlap)) {
8
8
  const end = Math.min(i + chunkSize, captions.length);
@@ -17,26 +17,51 @@ function splitIntoOverlappingChunks(captions, chunkSize = 20, overlap = 3) {
17
17
  return chunks;
18
18
  }
19
19
 
20
- function selectBestTranslation(translations, startIndex, endIndex) {
21
- // If we only have one translation for this caption, use it
22
- if (translations.length === 1) return translations[0];
20
+ export function selectBestTranslation(translations, startIndex, endIndex) {
21
+ try {
22
+ if (!translations || !Array.isArray(translations)) {
23
+ logger.warn(`Invalid translations input: ${JSON.stringify(translations)}`);
24
+ return null;
25
+ }
26
+
27
+ if (translations.length === 0) {
28
+ logger.warn(`No translations available for selection`);
29
+ return null;
30
+ }
31
+
32
+ // If we only have one translation for this caption, use it
33
+ if (translations.length === 1) return translations[0];
23
34
 
24
- // For multiple translations, prefer the one from the middle of its chunk
25
- // This helps avoid edge effects in translation
26
- return translations.reduce((best, current) => {
27
- const currentDistance = Math.min(
28
- Math.abs(current.chunkStart - startIndex),
29
- Math.abs(current.chunkEnd - endIndex)
30
- );
31
- const bestDistance = Math.min(
32
- Math.abs(best.chunkStart - startIndex),
33
- Math.abs(best.chunkEnd - endIndex)
34
- );
35
- return currentDistance < bestDistance ? current : best;
36
- });
35
+ // Use the first translation as a starting point
36
+ const first = translations[0];
37
+
38
+ // For multiple translations, prefer the one whose identifier is closest to the middle
39
+ // of the requested range
40
+ const targetValue = (Number(startIndex) + Number(endIndex)) / 2;
41
+
42
+ return translations.reduce((best, current) => {
43
+ try {
44
+ // Use identifier for comparison if available, otherwise use index
45
+ const currentValue = Number(current.identifier !== undefined ? current.identifier : current.index || 0);
46
+ const bestValue = Number(best.identifier !== undefined ? best.identifier : best.index || 0);
47
+
48
+ const currentDistance = Math.abs(currentValue - targetValue);
49
+ const bestDistance = Math.abs(bestValue - targetValue);
50
+
51
+ return currentDistance < bestDistance ? current : best;
52
+ } catch (err) {
53
+ logger.warn(`Error comparing translations: ${err.message}`);
54
+ return best; // Fallback to existing best on error
55
+ }
56
+ }, first);
57
+ } catch (err) {
58
+ logger.error(`Error in selectBestTranslation: ${err.message}`);
59
+ // Return the first translation if available, otherwise null
60
+ return translations && translations.length ? translations[0] : null;
61
+ }
37
62
  }
38
63
 
39
- async function translateChunk(chunk, args, maxRetries = 3) {
64
+ export async function translateChunk(chunk, args, maxRetries = 3) {
40
65
  const chunkText = build(chunk.captions, { format: args.format, preserveIndexes: true });
41
66
 
42
67
  for (let attempt = 0; attempt < maxRetries; attempt++) {
@@ -82,7 +107,7 @@ export default {
82
107
  timeout: 3600,
83
108
  executePathway: async ({args}) => {
84
109
  try {
85
- const { text, format = 'srt' } = args;
110
+ const { text, format = 'vtt' } = args;
86
111
  const parsed = parse(text, { format, preserveIndexes: true });
87
112
  const captions = parsed.cues;
88
113
 
@@ -101,16 +126,18 @@ export default {
101
126
  // Create a map of caption index to all its translations
102
127
  const translationMap = new Map();
103
128
  translatedChunks.flat().forEach(caption => {
104
- if (!translationMap.has(caption.index)) {
105
- translationMap.set(caption.index, []);
129
+ const identifier = caption.identifier || caption.index;
130
+ if (!translationMap.has(identifier)) {
131
+ translationMap.set(identifier, []);
106
132
  }
107
- translationMap.get(caption.index).push(caption);
133
+ translationMap.get(identifier).push(caption);
108
134
  });
109
135
 
110
136
  // Select best translation for each caption
111
137
  const finalCaptions = captions.map(caption => {
112
- const translations = translationMap.get(caption.index) || [caption];
113
- const bestTranslation = selectBestTranslation(translations, caption.index, caption.index);
138
+ const identifier = caption.identifier || caption.index;
139
+ const translations = translationMap.get(identifier) || [caption];
140
+ const bestTranslation = selectBestTranslation(translations, identifier, identifier);
114
141
  const text = bestTranslation?.text || caption?.text;
115
142
  return { ...caption, text };
116
143
  });
package/server/parser.js CHANGED
@@ -16,7 +16,7 @@ const parseNumberedList = (str) => {
16
16
  async function parseNumberedObjectList(text, format) {
17
17
  const parsedList = await callPathway('sys_parse_numbered_object_list', { text, format });
18
18
  try {
19
- return JSON.parse(parsedList);
19
+ return JSON.parse(parsedList) || [];
20
20
  } catch (error) {
21
21
  logger.warn(`Failed to parse numbered object list: ${error.message}`);
22
22
  return [];
@@ -72,13 +72,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
72
72
  if(maxLineWidth) tsparams.max_line_width = maxLineWidth;
73
73
  if(maxLineCount) tsparams.max_line_count = maxLineCount;
74
74
  if(maxWordsPerLine) tsparams.max_words_per_line = maxWordsPerLine;
75
- if(wordTimestamped!=null) {
76
- if(!wordTimestamped) {
77
- tsparams.word_timestamps = "False";
78
- }else{
79
- tsparams.word_timestamps = wordTimestamped;
80
- }
81
- }
75
+ tsparams.word_timestamps = !wordTimestamped ? "False" : wordTimestamped;
82
76
 
83
77
  const cortexRequest = new CortexRequest({ pathwayResolver });
84
78
  cortexRequest.url = WHISPER_TS_API_URL;
@@ -157,7 +151,8 @@ async function processURI(uri) {
157
151
 
158
152
  const intervalId = setInterval(() => sendProgress(true), 3000);
159
153
 
160
- const useTS = WHISPER_TS_API_URL && (wordTimestamped || highlightWords);
154
+ //const useTS = WHISPER_TS_API_URL && (wordTimestamped || highlightWords); // use TS API only for word timestamped
155
+ const useTS = !!WHISPER_TS_API_URL; // use TS API always if URL is set
161
156
 
162
157
  if (useTS) {
163
158
  _promise = processTS;
package/server/rest.js CHANGED
@@ -48,7 +48,7 @@ const processRestRequest = async (server, req, pathway, name, parameterMap = {})
48
48
  return value.map(msg => ({
49
49
  ...msg,
50
50
  content: Array.isArray(msg.content) ?
51
- JSON.stringify(msg.content) :
51
+ msg.content.map(item => JSON.stringify(item)) :
52
52
  msg.content
53
53
  }));
54
54
  } else {
@@ -212,3 +212,57 @@ test('convertMessagesToClaudeVertex user message with no content', async (t) =>
212
212
  t.deepEqual(output, { system: '', modifiedMessages: [] });
213
213
  });
214
214
 
215
+ test('convertMessagesToClaudeVertex with multi-part content array', async (t) => {
216
+ const plugin = new Claude3VertexPlugin(pathway, model);
217
+
218
+ // Test with multi-part content array
219
+ const multiPartContent = [
220
+ {
221
+ type: 'text',
222
+ text: 'Hello world'
223
+ },
224
+ {
225
+ type: 'text',
226
+ text: 'Hello2 world2'
227
+ },
228
+ {
229
+ type: 'image_url',
230
+ image_url: 'https://static.toiimg.com/thumb/msid-102827471,width-1280,height-720,resizemode-4/102827471.jpg'
231
+ }
232
+ ];
233
+
234
+ const messages = [
235
+ { role: 'system', content: 'System message' },
236
+ { role: 'user', content: multiPartContent }
237
+ ];
238
+
239
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
240
+
241
+ // Verify system message is preserved
242
+ t.is(output.system, 'System message');
243
+
244
+ // Verify the user message role is preserved
245
+ t.is(output.modifiedMessages[0].role, 'user');
246
+
247
+ // Verify the content array has the correct number of items
248
+ // We expect 3 items: 2 text items and 1 image item
249
+ t.is(output.modifiedMessages[0].content.length, 3);
250
+
251
+ // Verify the text content items
252
+ t.is(output.modifiedMessages[0].content[0].type, 'text');
253
+ t.is(output.modifiedMessages[0].content[0].text, 'Hello world');
254
+
255
+ t.is(output.modifiedMessages[0].content[1].type, 'text');
256
+ t.is(output.modifiedMessages[0].content[1].text, 'Hello2 world2');
257
+
258
+ // Verify the image content item
259
+ t.is(output.modifiedMessages[0].content[2].type, 'image');
260
+ t.is(output.modifiedMessages[0].content[2].source.type, 'base64');
261
+ t.is(output.modifiedMessages[0].content[2].source.media_type, 'image/jpeg');
262
+
263
+ // Check if the base64 data looks reasonable
264
+ const base64Data = output.modifiedMessages[0].content[2].source.data;
265
+ const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
266
+ t.true(base64Data.length > 100); // Check if the data is sufficiently long
267
+ t.true(base64Regex.test(base64Data)); // Check if the data matches the base64 regex
268
+ });
@@ -496,4 +496,129 @@ test('POST /chat/completions should return complete responses from gpt-4o', asyn
496
496
  const content = response.body.choices[0].message.content;
497
497
  t.regex(content, /END_MARKER_XYZ$/);
498
498
  });
499
+
500
+ test('POST /chat/completions should handle array content properly', async (t) => {
501
+ // This test verifies the functionality in server/rest.js where array content is JSON stringified
502
+ // Specifically testing: content: Array.isArray(msg.content) ? msg.content.map(item => JSON.stringify(item)) : msg.content
503
+
504
+ // Create a request with MultiMessage array content
505
+ const testContent = [
506
+ {
507
+ type: 'text',
508
+ text: 'Hello world'
509
+ },
510
+ {
511
+ type: 'text',
512
+ text: 'Hello2 world2'
513
+ },
514
+ {
515
+ type: 'image',
516
+ url: 'https://example.com/test.jpg'
517
+ }
518
+ ];
519
+
520
+ try {
521
+ // First, check if the API server is running and get available models
522
+ let modelToUse = '*'; // Default fallback model
523
+ try {
524
+ const modelsResponse = await got(`${API_BASE}/models`, { responseType: 'json' });
525
+ if (modelsResponse.body && modelsResponse.body.data && modelsResponse.body.data.length > 0) {
526
+ const models = modelsResponse.body.data.map(model => model.id);
527
+
528
+ // Priority 1: Find sonnet with highest version (e.g., claude-3.7-sonnet)
529
+ const sonnetVersions = models
530
+ .filter(id => id.includes('-sonnet') && id.startsWith('claude-'))
531
+ .sort((a, b) => {
532
+ // Extract version numbers and compare
533
+ const versionA = a.match(/claude-(\d+\.\d+)-sonnet/);
534
+ const versionB = b.match(/claude-(\d+\.\d+)-sonnet/);
535
+ if (versionA && versionB) {
536
+ return parseFloat(versionB[1]) - parseFloat(versionA[1]); // Descending order
537
+ }
538
+ return 0;
539
+ });
540
+
541
+ if (sonnetVersions.length > 0) {
542
+ modelToUse = sonnetVersions[0]; // Use highest version sonnet
543
+ } else {
544
+ // Priority 2: Any model ending with -sonnet
545
+ const anySonnet = models.find(id => id.endsWith('-sonnet'));
546
+ if (anySonnet) {
547
+ modelToUse = anySonnet;
548
+ } else {
549
+ // Priority 3: Any model starting with claude-
550
+ const anyClaude = models.find(id => id.startsWith('claude-'));
551
+ if (anyClaude) {
552
+ modelToUse = anyClaude;
553
+ } else {
554
+ // Fallback: Just use the first available model
555
+ modelToUse = models[0];
556
+ }
557
+ }
558
+ }
559
+
560
+ t.log(`Using model: ${modelToUse}`);
561
+ }
562
+ } catch (modelError) {
563
+ t.log('Could not get available models, using default model');
564
+ }
565
+
566
+ // Make a direct HTTP request to the REST API
567
+ const response = await axios.post(`${API_BASE}/chat/completions`, {
568
+ model: modelToUse,
569
+ messages: [
570
+ {
571
+ role: 'user',
572
+ content: testContent
573
+ }
574
+ ]
575
+ });
576
+
577
+ t.log('Response:', response.data.choices[0].message);
578
+
579
+ const message = response.data.choices[0].message;
580
+
581
+ //message should not have anything similar to:
582
+ //Execution failed for sys_claude_37_sonnet: HTTP error: 400 Bad Request
583
+ //HTTP error:
584
+ t.falsy(message.content.startsWith('HTTP error:'));
585
+ //400 Bad Request
586
+ t.falsy(message.content.startsWith('400 Bad Request'));
587
+ //Execution failed
588
+ t.falsy(message.content.startsWith('Execution failed'));
589
+ //Invalid JSON
590
+ t.falsy(message.content.startsWith('Invalid JSON'));
591
+
592
+
593
+ // If the request succeeds, it means the array content was properly processed
594
+ // If the JSON.stringify was not applied correctly, the request would fail
595
+ t.truthy(response.data);
596
+ t.pass('REST API successfully processed array content');
597
+ } catch (error) {
598
+ // If there's a connection error (e.g., API not running), we'll skip this test
599
+ if (error.code === 'ECONNREFUSED') {
600
+ t.pass('Skipping test - REST API not available');
601
+ } else {
602
+ // Check if the error response contains useful information
603
+ if (error.response) {
604
+ // We got a response from the server, but with an error status
605
+ t.log('Server responded with:', error.response.data);
606
+
607
+ // Skip the test if the server is running but no pathway is configured to handle the request
608
+ if (error.response.status === 404 &&
609
+ error.response.data.error &&
610
+ error.response.data.error.includes('not found')) {
611
+ t.pass('Skipping test - No suitable pathway configured for this API endpoint');
612
+ } else {
613
+ t.fail(`API request failed with status ${error.response.status}: ${error.response.statusText}`);
614
+ }
615
+ } else {
616
+ // No response received
617
+ t.fail(`API request failed: ${error.message}`);
618
+ }
619
+ }
620
+ }
621
+ });
622
+
623
+
499
624