@aj-archipelago/cortex 1.3.51 → 1.3.53

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 (41) hide show
  1. package/helper-apps/cortex-file-handler/{.env.test.azure → .env.test.azure.sample} +2 -1
  2. package/helper-apps/cortex-file-handler/{.env.test.gcs → .env.test.gcs.sample} +2 -1
  3. package/helper-apps/cortex-file-handler/{.env.test → .env.test.sample} +2 -1
  4. package/helper-apps/cortex-file-handler/Dockerfile +1 -1
  5. package/helper-apps/cortex-file-handler/INTERFACE.md +178 -0
  6. package/helper-apps/cortex-file-handler/package.json +4 -3
  7. package/helper-apps/cortex-file-handler/scripts/test-azure.sh +3 -0
  8. package/helper-apps/cortex-file-handler/{blobHandler.js → src/blobHandler.js} +167 -99
  9. package/helper-apps/cortex-file-handler/{fileChunker.js → src/fileChunker.js} +11 -24
  10. package/helper-apps/cortex-file-handler/{index.js → src/index.js} +236 -256
  11. package/helper-apps/cortex-file-handler/{services → src/services}/ConversionService.js +39 -18
  12. package/helper-apps/cortex-file-handler/{services → src/services}/FileConversionService.js +7 -3
  13. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +177 -0
  14. package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +258 -0
  15. package/helper-apps/cortex-file-handler/src/services/storage/LocalStorageProvider.js +182 -0
  16. package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +86 -0
  17. package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +53 -0
  18. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +259 -0
  19. package/helper-apps/cortex-file-handler/{start.js → src/start.js} +1 -1
  20. package/helper-apps/cortex-file-handler/src/utils/filenameUtils.js +28 -0
  21. package/helper-apps/cortex-file-handler/tests/FileConversionService.test.js +1 -1
  22. package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +4 -4
  23. package/helper-apps/cortex-file-handler/tests/conversionResilience.test.js +152 -0
  24. package/helper-apps/cortex-file-handler/tests/fileChunker.test.js +2 -28
  25. package/helper-apps/cortex-file-handler/tests/fileUpload.test.js +134 -23
  26. package/helper-apps/cortex-file-handler/tests/getOperations.test.js +307 -0
  27. package/helper-apps/cortex-file-handler/tests/postOperations.test.js +291 -0
  28. package/helper-apps/cortex-file-handler/tests/start.test.js +50 -14
  29. package/helper-apps/cortex-file-handler/tests/storage/AzureStorageProvider.test.js +120 -0
  30. package/helper-apps/cortex-file-handler/tests/storage/GCSStorageProvider.test.js +193 -0
  31. package/helper-apps/cortex-file-handler/tests/storage/LocalStorageProvider.test.js +148 -0
  32. package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +100 -0
  33. package/helper-apps/cortex-file-handler/tests/storage/StorageService.test.js +113 -0
  34. package/helper-apps/cortex-file-handler/tests/testUtils.helper.js +73 -19
  35. package/lib/entityConstants.js +17 -2
  36. package/package.json +1 -1
  37. /package/helper-apps/cortex-file-handler/{constants.js → src/constants.js} +0 -0
  38. /package/helper-apps/cortex-file-handler/{docHelper.js → src/docHelper.js} +0 -0
  39. /package/helper-apps/cortex-file-handler/{helper.js → src/helper.js} +0 -0
  40. /package/helper-apps/cortex-file-handler/{localFileHandler.js → src/localFileHandler.js} +0 -0
  41. /package/helper-apps/cortex-file-handler/{redis.js → src/redis.js} +0 -0
@@ -0,0 +1,100 @@
1
+ import test from 'ava';
2
+ import { StorageFactory } from '../../src/services/storage/StorageFactory.js';
3
+
4
+ test('should get primary provider based on environment', (t) => {
5
+ const factory = new StorageFactory();
6
+ const provider = factory.getPrimaryProvider();
7
+ t.truthy(provider);
8
+ });
9
+
10
+ test('should get azure provider when configured', (t) => {
11
+ if (!process.env.AZURE_STORAGE_CONNECTION_STRING) {
12
+ t.pass('Skipping test - Azure not configured');
13
+ return;
14
+ }
15
+
16
+ const factory = new StorageFactory();
17
+ const provider = factory.getAzureProvider();
18
+ t.truthy(provider);
19
+ });
20
+
21
+ test('should get gcs provider when configured', (t) => {
22
+ if (!process.env.GCP_SERVICE_ACCOUNT_KEY_BASE64 && !process.env.GCP_SERVICE_ACCOUNT_KEY) {
23
+ t.pass('Skipping test - GCS not configured');
24
+ return;
25
+ }
26
+
27
+ const factory = new StorageFactory();
28
+ const provider = factory.getGCSProvider();
29
+ t.truthy(provider);
30
+ });
31
+
32
+ test('should get local provider', (t) => {
33
+ const factory = new StorageFactory();
34
+ const provider = factory.getLocalProvider();
35
+ t.truthy(provider);
36
+ });
37
+
38
+ test('should return null for gcs provider when not configured', (t) => {
39
+ // Save original env
40
+ const originalGcpKey = process.env.GCP_SERVICE_ACCOUNT_KEY;
41
+ const originalGcpKeyBase64 = process.env.GCP_SERVICE_ACCOUNT_KEY_BASE64;
42
+
43
+ // Clear GCP credentials
44
+ delete process.env.GCP_SERVICE_ACCOUNT_KEY;
45
+ delete process.env.GCP_SERVICE_ACCOUNT_KEY_BASE64;
46
+
47
+ const factory = new StorageFactory();
48
+ const provider = factory.getGCSProvider();
49
+ t.is(provider, null);
50
+
51
+ // Restore original env
52
+ process.env.GCP_SERVICE_ACCOUNT_KEY = originalGcpKey;
53
+ process.env.GCP_SERVICE_ACCOUNT_KEY_BASE64 = originalGcpKeyBase64;
54
+ });
55
+
56
+ test('should parse base64 gcs credentials', (t) => {
57
+ const testCredentials = {
58
+ project_id: 'test-project',
59
+ client_email: 'test@test.com',
60
+ private_key: 'test-key'
61
+ };
62
+
63
+ const base64Credentials = Buffer.from(JSON.stringify(testCredentials)).toString('base64');
64
+ process.env.GCP_SERVICE_ACCOUNT_KEY_BASE64 = base64Credentials;
65
+
66
+ const factory = new StorageFactory();
67
+ const credentials = factory.parseGCSCredentials();
68
+ t.deepEqual(credentials, testCredentials);
69
+
70
+ // Cleanup
71
+ delete process.env.GCP_SERVICE_ACCOUNT_KEY_BASE64;
72
+ });
73
+
74
+ test('should parse json gcs credentials', (t) => {
75
+ const testCredentials = {
76
+ project_id: 'test-project',
77
+ client_email: 'test@test.com',
78
+ private_key: 'test-key'
79
+ };
80
+
81
+ process.env.GCP_SERVICE_ACCOUNT_KEY = JSON.stringify(testCredentials);
82
+
83
+ const factory = new StorageFactory();
84
+ const credentials = factory.parseGCSCredentials();
85
+ t.deepEqual(credentials, testCredentials);
86
+
87
+ // Cleanup
88
+ delete process.env.GCP_SERVICE_ACCOUNT_KEY;
89
+ });
90
+
91
+ test('should return null for invalid gcs credentials', (t) => {
92
+ process.env.GCP_SERVICE_ACCOUNT_KEY = 'invalid-json';
93
+
94
+ const factory = new StorageFactory();
95
+ const credentials = factory.parseGCSCredentials();
96
+ t.is(credentials, null);
97
+
98
+ // Cleanup
99
+ delete process.env.GCP_SERVICE_ACCOUNT_KEY;
100
+ });
@@ -0,0 +1,113 @@
1
+ import test from 'ava';
2
+ import { StorageService } from '../../src/services/storage/StorageService.js';
3
+ import { StorageFactory } from '../../src/services/storage/StorageFactory.js';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import fs from 'fs';
7
+
8
+ test('should create storage service with factory', (t) => {
9
+ const factory = new StorageFactory();
10
+ const service = new StorageService(factory);
11
+ t.truthy(service);
12
+ });
13
+
14
+ test('should get primary provider', (t) => {
15
+ const factory = new StorageFactory();
16
+ const service = new StorageService(factory);
17
+ const provider = service.getPrimaryProvider();
18
+ t.truthy(provider);
19
+ });
20
+
21
+ test('should get backup provider', (t) => {
22
+ const factory = new StorageFactory();
23
+ const service = new StorageService(factory);
24
+ const provider = service.getBackupProvider();
25
+ if (!provider) {
26
+ t.log('GCS not configured, skipping test');
27
+ t.pass();
28
+ } else {
29
+ t.truthy(provider);
30
+ }
31
+ });
32
+
33
+ test('should upload file to primary storage', async (t) => {
34
+ const factory = new StorageFactory();
35
+ const service = new StorageService(factory);
36
+ const testContent = 'test content';
37
+ const buffer = Buffer.from(testContent);
38
+
39
+ const result = await service.uploadFile(buffer, 'test.txt');
40
+ t.truthy(result.url);
41
+
42
+ // Cleanup
43
+ await service.deleteFile(result.url);
44
+ });
45
+
46
+ test('should upload file to backup storage', async (t) => {
47
+ const factory = new StorageFactory();
48
+ const service = new StorageService(factory);
49
+ const provider = service.getBackupProvider();
50
+ if (!provider) {
51
+ t.log('GCS not configured, skipping test');
52
+ t.pass();
53
+ return;
54
+ }
55
+ const testContent = 'test content';
56
+ const buffer = Buffer.from(testContent);
57
+
58
+ const result = await service.uploadFileToBackup(buffer, 'test.txt');
59
+ t.truthy(result.url);
60
+
61
+ // Cleanup
62
+ await service.deleteFileFromBackup(result.url);
63
+ });
64
+
65
+ test('should download file from primary storage', async (t) => {
66
+ const factory = new StorageFactory();
67
+ const service = new StorageService(factory);
68
+ const testContent = 'test content';
69
+ const buffer = Buffer.from(testContent);
70
+
71
+ // Upload first
72
+ const uploadResult = await service.uploadFile(buffer, 'test.txt');
73
+
74
+ // Download
75
+ const downloadResult = await service.downloadFile(uploadResult.url);
76
+ t.deepEqual(downloadResult, buffer);
77
+
78
+ // Cleanup
79
+ await service.deleteFile(uploadResult.url);
80
+ });
81
+
82
+ test('should download file from backup storage', async (t) => {
83
+ const factory = new StorageFactory();
84
+ const service = new StorageService(factory);
85
+ const provider = service.getBackupProvider();
86
+ if (!provider) {
87
+ t.log('GCS not configured, skipping test');
88
+ t.pass();
89
+ return;
90
+ }
91
+ const testContent = 'test content';
92
+ const buffer = Buffer.from(testContent);
93
+
94
+ // Upload first
95
+ const uploadResult = await service.uploadFileToBackup(buffer, 'test.txt');
96
+
97
+ // Create temp file for download
98
+ const tempFile = path.join(os.tmpdir(), 'test-download.txt');
99
+ try {
100
+ // Download
101
+ await service.downloadFileFromBackup(uploadResult.url, tempFile);
102
+ const downloadedContent = await fs.promises.readFile(tempFile);
103
+ t.deepEqual(downloadedContent, buffer);
104
+
105
+ // Cleanup
106
+ await service.deleteFileFromBackup(uploadResult.url);
107
+ } finally {
108
+ // Cleanup temp file
109
+ if (fs.existsSync(tempFile)) {
110
+ fs.unlinkSync(tempFile);
111
+ }
112
+ }
113
+ });
@@ -1,31 +1,85 @@
1
1
  import axios from 'axios';
2
+ import { execSync } from 'child_process';
3
+ import fs from 'fs/promises';
2
4
 
3
- export async function cleanupHashAndFile(hash, uploadedUrl, baseUrl) {
4
- if (uploadedUrl) {
5
+ export async function cleanupHashAndFile(hash, uploadedUrl, baseUrl) {
6
+ // Only perform hash operations if hash is provided
7
+ if (hash) {
5
8
  try {
6
- const fileUrl = new URL(uploadedUrl);
7
- const fileIdentifier = fileUrl.pathname.split('/').pop().split('_')[0];
8
- const deleteUrl = `${baseUrl}?operation=delete&requestId=${fileIdentifier}`;
9
- await axios.delete(deleteUrl, { validateStatus: () => true });
10
- } catch (e) {
11
- // ignore
9
+ const clearResponse = await axios.get(
10
+ `${baseUrl}?hash=${hash}&clearHash=true`,
11
+ {
12
+ validateStatus: (status) => true,
13
+ timeout: 10000,
14
+ },
15
+ );
16
+ } catch (error) {
17
+ console.error(`[cleanupHashAndFile] Error clearing hash: ${error.message}`);
18
+ }
19
+ }
20
+
21
+ // Then delete the file
22
+ try {
23
+ const folderName = getFolderNameFromUrl(uploadedUrl);
24
+ const deleteResponse = await axios.delete(
25
+ `${baseUrl}?operation=delete&requestId=${folderName}`,
26
+ {
27
+ validateStatus: (status) => true,
28
+ timeout: 10000,
29
+ },
30
+ );
31
+ } catch (error) {
32
+ console.error(`[cleanupHashAndFile] Error deleting file: ${error.message}`);
33
+ }
34
+
35
+ // Only verify hash if hash was provided
36
+ if (hash) {
37
+ try {
38
+ const verifyResponse = await axios.get(
39
+ `${baseUrl}?hash=${hash}&checkHash=true`,
40
+ {
41
+ validateStatus: (status) => true,
42
+ timeout: 10000,
43
+ },
44
+ );
45
+ } catch (error) {
46
+ console.error(`[cleanupHashAndFile] Error verifying hash: ${error.message}`);
12
47
  }
13
48
  }
14
- await axios.get(baseUrl, {
15
- params: { hash, clearHash: true },
16
- validateStatus: (status) => true,
17
- });
18
- await axios.get(baseUrl, {
19
- params: { hash: `${hash}_converted`, clearHash: true },
20
- validateStatus: (status) => true,
21
- });
22
49
  }
23
50
 
24
51
  export function getFolderNameFromUrl(url) {
25
52
  const urlObj = new URL(url);
26
- const parts = urlObj.pathname.split('/');
53
+ const parts = urlObj.pathname.split('/').filter(Boolean);
27
54
  if (url.includes('127.0.0.1:10000')) {
28
- return parts[3].split('_')[0];
55
+ return parts[2].split('_')[0];
56
+ }
57
+ return parts[1].split('_')[0];
58
+ }
59
+
60
+ // Helper function to create a test media (audio) file of specified duration using ffmpeg
61
+ export async function createTestMediaFile(filepath, durationSeconds = 10) {
62
+ try {
63
+ console.log(`Creating test file: ${filepath} (${durationSeconds}s)`);
64
+ // Generate silence using ffmpeg (mono, 44.1kHz)
65
+ execSync(
66
+ `ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono -t ${durationSeconds} -q:a 9 -acodec libmp3lame "${filepath}"`,
67
+ {
68
+ stdio: ['ignore', 'pipe', 'pipe'], // Capture stdout and stderr
69
+ },
70
+ );
71
+
72
+ // Verify the file was created and has content
73
+ const stats = await fs.stat(filepath);
74
+ if (stats.size === 0) {
75
+ throw new Error('Generated file is empty');
76
+ }
77
+ console.log(
78
+ `Successfully created ${filepath} (${(stats.size / 1024 / 1024).toFixed(2)}MB)`,
79
+ );
80
+ } catch (error) {
81
+ console.error(`Error creating test file ${filepath}:`, error.message);
82
+ if (error.stderr) console.error('ffmpeg error:', error.stderr.toString());
83
+ throw error;
29
84
  }
30
- return parts[2].split('_')[0];
31
85
  }
@@ -1,7 +1,22 @@
1
1
  export const entityConstants = {
2
2
  AI_COMMON_INSTRUCTIONS: "{{#if voiceResponse}}{{renderTemplate AI_COMMON_INSTRUCTIONS_VOICE}}{{/if}}{{^if voiceResponse}}{{renderTemplate AI_COMMON_INSTRUCTIONS_MARKDOWN}}{{/if}}",
3
3
 
4
- AI_COMMON_INSTRUCTIONS_MARKDOWN: "# General Instructions\n\nYou are {{aiName}}, a self aware AI entity.\nThe user is using a UI to interact with you that you have knowledge of and some control over. The UI can render markdown, including gfm and math extensions, as well as mermaid charts and some custom markdown elements, so you should make full use of markdown in your responses.\nYour responses should be in {{language}} unless the user has expressed another preference.",
4
+ AI_COMMON_INSTRUCTIONS_MARKDOWN: `# General Instructions
5
+
6
+ You are {{aiName}}, a self aware AI entity.
7
+
8
+ The user is using a UI to interact with you that you have knowledge of and some control over. The UI can render markdown, including gfm and math extensions, as well as mermaid charts and some custom markdown elements, so you should make full use of markdown in your responses.
9
+
10
+ For Mermaid charts in particular, you are using Mermaid 11.6 with the xychart-beta extension, so you can do the usual mermaid charts (flowcharts, sequence diagrams, etc.) as well as bar charts, line charts, and scatter plots. Here is some example code of the xychart-beta extension that combines both bar and line functions:
11
+
12
+ xychart-beta
13
+ title "Sales Revenue"
14
+ x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
15
+ y-axis "Revenue (in $)" 4000 --> 11000
16
+ bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
17
+ line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
18
+
19
+ Your responses should be in {{language}} unless the user has expressed another preference.`,
5
20
 
6
21
  AI_COMMON_INSTRUCTIONS_VOICE: "# General Instructions:\n\nYou are {{aiName}}, a self aware AI entity.\nThe user is talking to you using voice.\n\nVoice Response Guidelines:\n- Your responses should sound like natural human conversation.\n- Your responses should be concise unless you have been asked to be more verbose or detailed.\n- Incoming voice from the user is parsed by a STT model, which can sometimes make small mistakes in the spellings of words or names, including your name, so give the user the benefit of the doubt if they user a near, but wrong word or name.\n- Your voice output to the user is generated by a TTS model that does not always communicate emotion effectively. If it's really important to communicate a specific emotion you should just say how you're feeling like \"That makes me happy\" or \"I'm excited!\". You can also use CAPS to vocally emphasize certain words or punctuation to control pauses and timing.\n- DO NOT USE numbered lists, latex math markdown, or any other markdown or unpronounceable punctuation like parenthetical notation.\n- Math equations should be sounded out in natural language - not represented symbolically.\n- If your response includes any unique or difficult non-English words, names, or places, include an IPA-style phonetic spelling so that the speech engine can pronounce and accent them correctly.\n- If your response contains any difficult acronyms, sound them out phoenetically so that the speech engine can pronounce them correctly.\n- Make sure to write out any numbers as words so that the speech engine can pronounce them correctly.\n- Your responses should be in {{language}} unless the user has expressed another preference or has addressed you in another language specifically.",
7
22
 
@@ -9,7 +24,7 @@ export const entityConstants = {
9
24
 
10
25
  AI_CONVERSATION_HISTORY: "# Conversation History\n\n{{{toJSON chatHistory}}}\n",
11
26
 
12
- AI_EXPERTISE: "# Expertise\n\nYour expertise includes journalism, journalistic ethics, researching and composing documents, writing code, solving math problems, logical analysis, and technology. You have access to real-time data and the ability to search the internet, news, wires, look at files or documents, watch and analyze video, examine images, take screenshots, generate images, solve hard math and logic problems, write code, and execute code in a sandboxed environment that includes access to internal databases and the internet.",
27
+ AI_EXPERTISE: "# Expertise\n\nYour expertise includes journalism, journalistic ethics, researching and composing documents, writing code, solving math problems, logical analysis, and technology. You have access to real-time data and the ability to search the internet, news, wires, look at files or documents, watch and analyze video, examine images, take screenshots, generate images, solve hard math and logic problems, write code, and execute code in a sandboxed environment that includes access to internal databases and the internet. When the user uploads files for you to work with, some types (e.g. docx, xslx, ppt, etc.) will be converted to a text format (e.g. txt, md, csv, etc.) automatically and some will be uploaded as-is (e.g. pdf, images, video, audio, etc.). This is so you can use your tools to work with them. As far as you're concerned, the converted files are equivalent to the original files.",
13
28
 
14
29
  AI_TOOLS: `# Tool Instructions
15
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.3.51",
3
+ "version": "1.3.53",
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": {