@aj-archipelago/cortex 1.1.34 → 1.1.36

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 CHANGED
@@ -503,7 +503,8 @@ The configuration should include the following properties:
503
503
  "awsAccessKeyId": "your_access_key_id", // Only for AWS S3
504
504
  "awsSecretAccessKey": "your_secret_access_key", // Only for AWS S3
505
505
  "awsRegion": "your_aws_region", // Only for AWS S3
506
- "awsBucketName": "cortexdynamicpathways" // Optional, default is "cortexdynamicpathways"
506
+ "awsBucketName": "cortexdynamicpathways", // Optional, default is "cortexdynamicpathways"
507
+ "publishKey": "your_publish_key"
507
508
  }
508
509
  ```
509
510
 
@@ -559,7 +560,7 @@ query ExecuteWorkspace($userId: String!, $pathwayName: String!, $text: String!)
559
560
 
560
561
  To ensure the security of dynamic pathways:
561
562
 
562
- 1. A `PATHWAY_PUBLISH_KEY` environment variable must be set to enable pathway publishing.
563
+ 1. A `publishKey` must be set in the dynamic pathways configuration to enable pathway publishing.
563
564
  2. This key must be provided in the `key` parameter when adding, updating, or deleting pathways.
564
565
  3. Each pathway is associated with a `userId` and `secret`. The secret must be provided to modify or delete an existing pathway.
565
566
 
@@ -567,4 +568,4 @@ To ensure the security of dynamic pathways:
567
568
 
568
569
  Each instance of Cortex maintains its own local cache of pathways. On every dynamic pathway request, it checks if the local cache is up to date by comparing the last modified timestamp of the storage with the last update time of the local cache. If the local cache is out of date, it reloads the pathways from storage.
569
570
 
570
- This approach ensures that all instances of Cortex will eventually have access to the most up-to-date dynamic pathways without requiring immediate synchronization.
571
+ This approach ensures that all instances of Cortex will eventually have access to the most up-to-date dynamic pathways without requiring immediate synchronization.
@@ -1,4 +1,5 @@
1
1
  {
2
2
  "storageType": "azure",
3
3
  "filePath": "./pathways/dynamic/pathways.json",
4
+ "publishKey": "development"
4
5
  }
package/config.js CHANGED
@@ -321,6 +321,7 @@ const createDynamicPathwayManager = async (config, basePathway) => {
321
321
  awsSecretAccessKey: dynamicPathwayConfig.awsSecretAccessKey,
322
322
  awsRegion: dynamicPathwayConfig.awsRegion,
323
323
  awsBucketName: dynamicPathwayConfig.awsBucketName || 'cortexdynamicpathways',
324
+ publishKey: dynamicPathwayConfig.publishKey,
324
325
  };
325
326
 
326
327
  const pathwayManager = new PathwayManager(storageConfig, basePathway);
@@ -1,8 +1,8 @@
1
1
  import { BlobServiceClient } from '@azure/storage-blob';
2
2
  import { S3 } from '@aws-sdk/client-s3';
3
3
  import fs from 'fs';
4
+ import path from 'path';
4
5
  import logger from './logger.js';
5
- const { PATHWAY_PUBLISH_KEY } = process.env;
6
6
 
7
7
  class StorageStrategy {
8
8
  async load() { throw new Error('Not implemented'); }
@@ -20,6 +20,8 @@ class LocalStorage extends StorageStrategy {
20
20
  if (!fs.existsSync(this.filePath)) {
21
21
  // create it. log
22
22
  logger.info(`Creating dynamic pathways local file: ${this.filePath}`);
23
+ // create directory if it doesn't exist
24
+ await fs.promises.mkdir(path.dirname(this.filePath), { recursive: true });
23
25
  await fs.promises.writeFile(this.filePath, JSON.stringify({}));
24
26
  }
25
27
 
@@ -80,7 +82,7 @@ class AzureBlobStorage extends StorageStrategy {
80
82
  }
81
83
 
82
84
  const downloadBlockBlobResponse = await blockBlobClient.download();
83
- const data = await this.streamToString(downloadBlockBlobResponse.readableStreamBody);
85
+ const data = await streamToString(downloadBlockBlobResponse.readableStreamBody);
84
86
  const parsedData = JSON.parse(data);
85
87
  logger.info(`Loaded pathways from Azure Blob Storage. ${Object.keys(parsedData).map(user => `${user}(${Object.keys(parsedData[user])})`).join(', ')}`);
86
88
  return parsedData;
@@ -124,18 +126,33 @@ class AzureBlobStorage extends StorageStrategy {
124
126
  class S3Storage extends StorageStrategy {
125
127
  constructor(config) {
126
128
  super();
127
- this.s3 = new S3(config);
128
- this.bucketName = config.bucketName;
129
+ this.s3 = new S3({
130
+ credentials: {
131
+ accessKeyId: config.awsAccessKeyId,
132
+ secretAccessKey: config.awsSecretAccessKey
133
+ },
134
+ region: config.awsRegion
135
+ });
136
+ this.bucketName = config.awsBucketName;
129
137
  }
130
138
 
131
139
  async load() {
132
140
  try {
141
+ // Check if bucket exists, create if it doesn't
142
+ await this.ensureBucketExists();
143
+
144
+ // Check if file exists, create if it doesn't
145
+ await this.ensureFileExists();
146
+
133
147
  const params = {
134
148
  Bucket: this.bucketName,
135
149
  Key: 'pathways.json'
136
150
  };
137
151
  const data = await this.s3.getObject(params);
138
- return JSON.parse(data.Body.toString());
152
+
153
+ const readableStream = data.Body;
154
+ const dataString = await streamToString(readableStream);
155
+ return JSON.parse(dataString);
139
156
  } catch (error) {
140
157
  logger.error('Error loading pathways from S3:', error);
141
158
  throw error;
@@ -164,11 +181,47 @@ class S3Storage extends StorageStrategy {
164
181
  const data = await this.s3.headObject(params);
165
182
  return new Date(data.LastModified).getTime();
166
183
  }
184
+
185
+ async ensureBucketExists() {
186
+ try {
187
+ await this.s3.headBucket({ Bucket: this.bucketName });
188
+ } catch (error) {
189
+ if (error.name === 'NotFound') {
190
+ logger.info(`Bucket ${this.bucketName} does not exist, creating it`);
191
+ await this.s3.createBucket({ Bucket: this.bucketName });
192
+ } else {
193
+ throw error;
194
+ }
195
+ }
196
+ }
197
+
198
+ async ensureFileExists() {
199
+ try {
200
+ await this.s3.headObject({
201
+ Bucket: this.bucketName,
202
+ Key: 'pathways.json'
203
+ });
204
+ } catch (error) {
205
+ if (error.name === 'NotFound') {
206
+ logger.info('pathways.json does not exist, creating it with empty object');
207
+ const emptyContent = JSON.stringify({});
208
+ await this.s3.putObject({
209
+ Bucket: this.bucketName,
210
+ Key: 'pathways.json',
211
+ Body: emptyContent,
212
+ ContentType: 'application/json'
213
+ });
214
+ } else {
215
+ throw error;
216
+ }
217
+ }
218
+ }
167
219
  }
168
220
 
169
221
  class PathwayManager {
170
222
  constructor(config, basePathway) {
171
223
  this.storage = this.getStorageStrategy(config);
224
+ this.publishKey = config.publishKey;
172
225
  this.pathways = {};
173
226
  this.lastUpdated = 0;
174
227
  this.basePathway = basePathway;
@@ -176,6 +229,10 @@ class PathwayManager {
176
229
  if (config.storageType === 'local') {
177
230
  logger.warn('WARNING: Local file storage is being used for dynamic pathways. If there are multiple instances of Cortex, they will not be synced. Consider using cloud storage such as S3 or Azure for production environments.');
178
231
  }
232
+
233
+ if (!this.publishKey) {
234
+ logger.warn('WARNING: dynamicPathwaysConfig.publishKey is not set. Dynamic pathways will not be editable in this instance of Cortex.');
235
+ }
179
236
  }
180
237
 
181
238
  getStorageStrategy(config) {
@@ -233,6 +290,7 @@ class PathwayManager {
233
290
  throw new Error('Both userId and secret are mandatory for adding or updating a pathway');
234
291
  }
235
292
 
293
+ await this.getLatestPathways();
236
294
  this.pathways[userId] = this.pathways[userId] || {};
237
295
 
238
296
  if (this.pathways[userId][name] && this.pathways[userId][name].secret !== secret) {
@@ -246,9 +304,12 @@ class PathwayManager {
246
304
  }
247
305
 
248
306
  async removePathway(name, userId, secret) {
307
+ await this.getLatestPathways();
308
+
249
309
  if (!this.pathways[userId] || !this.pathways[userId][name]) {
250
310
  return;
251
311
  }
312
+
252
313
  if (this.pathways[userId][name].secret !== secret) {
253
314
  throw new Error('Invalid secret');
254
315
  }
@@ -294,11 +355,11 @@ class PathwayManager {
294
355
  return {
295
356
  Mutation: {
296
357
  putPathway: async (_, { name, pathway, userId, secret, displayName, key }) => {
297
- if (!PATHWAY_PUBLISH_KEY) {
358
+ if (!this.publishKey) {
298
359
  throw new Error("Invalid configuration. Pathway publishing key is not configured in Cortex.")
299
360
  }
300
361
 
301
- if (key !== PATHWAY_PUBLISH_KEY) {
362
+ if (key !== this.publishKey) {
302
363
  throw new Error('Invalid pathway publishing key. The key provided did not match the key configured in Cortex.');
303
364
  }
304
365
 
@@ -310,10 +371,10 @@ class PathwayManager {
310
371
  }
311
372
  },
312
373
  deletePathway: async (_, { name, userId, secret, key }) => {
313
- if (!PATHWAY_PUBLISH_KEY) {
374
+ if (!this.publishKey) {
314
375
  throw new Error("Invalid configuration. Pathway publishing key is not configured in Cortex.")
315
376
  }
316
- if (key !== PATHWAY_PUBLISH_KEY) {
377
+ if (key !== this.publishKey) {
317
378
  throw new Error('Invalid pathway publishing key. The key provided did not match the key configured in Cortex.');
318
379
  }
319
380
 
@@ -328,7 +389,7 @@ class PathwayManager {
328
389
  };
329
390
  }
330
391
 
331
- async getPathways() {
392
+ async getLatestPathways() {
332
393
  try {
333
394
  const currentTimestamp = await this.storage.getLastModified();
334
395
 
@@ -340,13 +401,13 @@ class PathwayManager {
340
401
 
341
402
  return this.pathways;
342
403
  } catch (error) {
343
- logger.error('Error in getPathways:', error);
404
+ logger.error('Error in getLatestPathways:', error);
344
405
  throw error;
345
406
  }
346
407
  }
347
408
 
348
409
  async getPathway(userId, pathwayName) {
349
- const pathways = await this.getPathways();
410
+ const pathways = await this.getLatestPathways();
350
411
 
351
412
  if (!pathways[userId] || !pathways[userId][pathwayName]) {
352
413
  throw new Error(`Pathway '${pathwayName}' not found for user '${userId}'`);
@@ -354,20 +415,19 @@ class PathwayManager {
354
415
 
355
416
  return pathways[userId][pathwayName];
356
417
  }
418
+ }
357
419
 
358
- // Helper function to convert a readable stream to a string
359
- async streamToString(readableStream) {
360
- return new Promise((resolve, reject) => {
361
- const chunks = [];
362
- readableStream.on('data', (data) => {
363
- chunks.push(data.toString());
364
- });
365
- readableStream.on('end', () => {
366
- resolve(chunks.join(''));
367
- });
368
- readableStream.on('error', reject);
420
+ // Helper function to convert a readable stream to a string
421
+ async function streamToString(readableStream) {
422
+ return new Promise((resolve, reject) => {
423
+ const chunks = [];
424
+ readableStream.on('data', (data) => {
425
+ chunks.push(data.toString());
369
426
  });
370
- }
427
+ readableStream.on('end', () => {
428
+ resolve(chunks.join(''));
429
+ });
430
+ readableStream.on('error', reject);
431
+ });
371
432
  }
372
-
373
- export default PathwayManager;
433
+ export default PathwayManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.1.34",
3
+ "version": "1.1.36",
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": {
@@ -27,17 +27,18 @@ class OpenAIVisionPlugin extends OpenAIChatPlugin {
27
27
  }
28
28
  if (Array.isArray(message.content)) {
29
29
  message.content = message.content.map(item => {
30
- if (typeof item === 'string') {
31
- const parsedItem = safeJsonParse(item);
32
- return parsedItem.type ? parsedItem : { type: 'text', text: item };
33
- } else if (typeof item === 'object') {
34
- const { type, image_url, url } = item;
35
- if (type === 'image_url') {
36
- image_url.url = url || image_url.url;
37
- return { type, image_url };
38
- }
30
+ const parsedItem = safeJsonParse(item);
31
+
32
+ if (typeof parsedItem === 'string') {
33
+ return { type: 'text', text: parsedItem };
34
+ }
35
+
36
+ if (typeof parsedItem === 'object' && parsedItem !== null && parsedItem.type === 'image_url') {
37
+ parsedItem.image_url.url = parsedItem.url || parsedItem.image_url.url;
38
+ return parsedItem;
39
39
  }
40
- return item;
40
+
41
+ return parsedItem;
41
42
  });
42
43
  }
43
44
  } catch (e) {
@@ -0,0 +1,71 @@
1
+ // replicateApiPlugin.js
2
+ import ModelPlugin from "./modelPlugin.js";
3
+ import logger from "../../lib/logger.js";
4
+
5
+ class ReplicateApiPlugin extends ModelPlugin {
6
+ constructor(pathway, model) {
7
+ super(pathway, model);
8
+ }
9
+
10
+ // Set up parameters specific to the Replicate API
11
+ getRequestParameters(text, parameters, prompt) {
12
+ const combinedParameters = { ...this.promptParameters, ...parameters };
13
+ const { modelPromptText } = this.getCompiledPrompt(
14
+ text,
15
+ parameters,
16
+ prompt,
17
+ );
18
+
19
+ const requestParameters = {
20
+ input: {
21
+ aspect_ratio: "1:1",
22
+ output_format: "webp",
23
+ output_quality: 80,
24
+ prompt: modelPromptText,
25
+ //prompt_upsampling: false,
26
+ //safety_tolerance: 5,
27
+ go_fast: true,
28
+ megapixels: "1",
29
+ num_outputs: combinedParameters.numberResults,
30
+ },
31
+ };
32
+
33
+ return requestParameters;
34
+ }
35
+
36
+ // Execute the request to the Replicate API
37
+ async execute(text, parameters, prompt, cortexRequest) {
38
+ const requestParameters = this.getRequestParameters(
39
+ text,
40
+ parameters,
41
+ prompt,
42
+ );
43
+
44
+ cortexRequest.data = requestParameters;
45
+ cortexRequest.params = requestParameters.params;
46
+
47
+ return this.executeRequest(cortexRequest);
48
+ }
49
+
50
+ // Parse the response from the Replicate API
51
+ parseResponse(data) {
52
+ if (data.data) {
53
+ return JSON.stringify(data.data);
54
+ }
55
+ return JSON.stringify(data);
56
+ }
57
+
58
+ // Override the logging function to display the request and response
59
+ logRequestData(data, responseData, prompt) {
60
+ const modelInput = data?.input?.prompt;
61
+
62
+ logger.verbose(`${modelInput}`);
63
+ logger.verbose(`${this.parseResponse(responseData)}`);
64
+
65
+ prompt &&
66
+ prompt.debugInfo &&
67
+ (prompt.debugInfo += `\n${JSON.stringify(data)}`);
68
+ }
69
+ }
70
+
71
+ export default ReplicateApiPlugin;