@aj-archipelago/cortex 0.0.5 → 0.0.7
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 +108 -72
- package/config.js +25 -0
- package/graphql/graphql.js +56 -13
- package/graphql/pathwayPrompter.js +10 -6
- package/graphql/pathwayResolver.js +128 -63
- package/graphql/plugins/azureTranslatePlugin.js +16 -8
- package/graphql/plugins/modelPlugin.js +67 -9
- package/graphql/plugins/openAiChatPlugin.js +34 -7
- package/graphql/plugins/openAiCompletionPlugin.js +53 -33
- package/graphql/plugins/openAiWhisperPlugin.js +79 -0
- package/graphql/prompt.js +1 -0
- package/graphql/requestState.js +5 -0
- package/graphql/resolver.js +8 -8
- package/graphql/subscriptions.js +15 -2
- package/graphql/typeDef.js +47 -38
- package/lib/fileChunker.js +152 -0
- package/lib/request.js +65 -8
- package/lib/requestMonitor.js +43 -0
- package/package.json +18 -6
- package/pathways/basePathway.js +3 -4
- package/pathways/bias.js +7 -0
- package/pathways/chat.js +4 -1
- package/pathways/complete.js +4 -0
- package/pathways/edit.js +6 -0
- package/pathways/entities.js +12 -0
- package/pathways/index.js +1 -1
- package/pathways/paraphrase.js +4 -0
- package/pathways/sentiment.js +5 -1
- package/pathways/summary.js +25 -8
- package/pathways/transcribe.js +8 -0
- package/pathways/translate.js +10 -1
- package/tests/chunking.test.js +5 -0
- package/tests/main.test.js +5 -13
- package/tests/translate.test.js +5 -0
- package/pathways/topics.js +0 -9
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// OpenAICompletionPlugin.js
|
|
2
|
+
const ModelPlugin = require('./modelPlugin');
|
|
3
|
+
const handlebars = require("handlebars");
|
|
4
|
+
const { encode } = require("gpt-3-encoder");
|
|
5
|
+
const FormData = require('form-data');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const { splitMediaFile, isValidYoutubeUrl, processYoutubeUrl, deleteTempPath } = require('../../lib/fileChunker');
|
|
8
|
+
const pubsub = require('../pubsub');
|
|
9
|
+
|
|
10
|
+
class OpenAIWhisperPlugin extends ModelPlugin {
|
|
11
|
+
constructor(config, pathway) {
|
|
12
|
+
super(config, pathway);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getCompiledPrompt(text, parameters, prompt) {
|
|
16
|
+
const combinedParameters = { ...this.promptParameters, ...parameters };
|
|
17
|
+
const modelPrompt = this.getModelPrompt(prompt, parameters);
|
|
18
|
+
const modelPromptText = modelPrompt.prompt ? handlebars.compile(modelPrompt.prompt)({ ...combinedParameters, text }) : '';
|
|
19
|
+
|
|
20
|
+
return { modelPromptText, tokenLength: encode(modelPromptText).length };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Execute the request to the OpenAI Whisper API
|
|
24
|
+
async execute(text, parameters, prompt, pathwayResolver) {
|
|
25
|
+
const url = this.requestUrl(text);
|
|
26
|
+
const params = {};
|
|
27
|
+
const { modelPromptText } = this.getCompiledPrompt(text, parameters, prompt);
|
|
28
|
+
|
|
29
|
+
const processChunk = async (chunk) => {
|
|
30
|
+
try {
|
|
31
|
+
const formData = new FormData();
|
|
32
|
+
formData.append('file', fs.createReadStream(chunk));
|
|
33
|
+
formData.append('model', this.model.params.model);
|
|
34
|
+
formData.append('response_format', 'text');
|
|
35
|
+
// formData.append('language', 'tr');
|
|
36
|
+
modelPromptText && formData.append('prompt', modelPromptText);
|
|
37
|
+
|
|
38
|
+
return this.executeRequest(url, formData, params, { ...this.model.headers, ...formData.getHeaders() });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.log(err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let result;
|
|
45
|
+
let { file } = parameters;
|
|
46
|
+
let folder;
|
|
47
|
+
const isYoutubeUrl = isValidYoutubeUrl(file);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
if (isYoutubeUrl) {
|
|
51
|
+
file = await processYoutubeUrl(file);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const mediaSplit = await splitMediaFile(file);
|
|
55
|
+
|
|
56
|
+
const { requestId } = pathwayResolver;
|
|
57
|
+
pubsub.publish('REQUEST_PROGRESS', {
|
|
58
|
+
requestProgress: {
|
|
59
|
+
requestId,
|
|
60
|
+
progress: 0.5,
|
|
61
|
+
data: null,
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
folder = mediaSplit.folder;
|
|
66
|
+
result = await Promise.all(mediaSplit.chunks.map(processChunk));
|
|
67
|
+
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error("An error occurred:", error);
|
|
70
|
+
} finally {
|
|
71
|
+
isYoutubeUrl && (await deleteTempPath(file));
|
|
72
|
+
folder && (await deleteTempPath(folder));
|
|
73
|
+
}
|
|
74
|
+
return result.join('');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = OpenAIWhisperPlugin;
|
|
79
|
+
|
package/graphql/prompt.js
CHANGED
package/graphql/resolver.js
CHANGED
|
@@ -5,23 +5,23 @@ const { PathwayResolver } = require("./pathwayResolver");
|
|
|
5
5
|
// (parent, args, contextValue, info)
|
|
6
6
|
const rootResolver = async (parent, args, contextValue, info) => {
|
|
7
7
|
const { config, pathway, requestState } = contextValue;
|
|
8
|
-
const { temperature } = pathway;
|
|
8
|
+
const { temperature, enableGraphqlCache } = pathway;
|
|
9
9
|
|
|
10
|
-
// Turn
|
|
11
|
-
if (temperature == 0) {
|
|
10
|
+
// Turn on graphql caching if enableGraphqlCache true and temperature is 0
|
|
11
|
+
if (enableGraphqlCache && temperature == 0) { // ||
|
|
12
12
|
info.cacheControl.setCacheHint({ maxAge: 60 * 60 * 24, scope: 'PUBLIC' });
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const pathwayResolver = new PathwayResolver({ config, pathway, requestState });
|
|
15
|
+
const pathwayResolver = new PathwayResolver({ config, pathway, args, requestState });
|
|
16
16
|
contextValue.pathwayResolver = pathwayResolver;
|
|
17
17
|
|
|
18
|
-
// Add request parameters back as debug
|
|
19
|
-
const requestParameters = pathwayResolver.prompts.map((prompt) => pathwayResolver.pathwayPrompter.plugin.requestParameters(args.text, args, prompt));
|
|
20
|
-
const debug = JSON.stringify(requestParameters);
|
|
21
|
-
|
|
22
18
|
// Execute the request with timeout
|
|
23
19
|
const result = await fulfillWithTimeout(pathway.resolver(parent, args, contextValue, info), pathway.timeout);
|
|
24
20
|
const { warnings, previousResult, savedContextId } = pathwayResolver;
|
|
21
|
+
|
|
22
|
+
// Add request parameters back as debug
|
|
23
|
+
const debug = pathwayResolver.prompts.map(prompt => prompt.debugInfo || '').join('\n').trim();
|
|
24
|
+
|
|
25
25
|
return { debug, result, warnings, previousResult, contextId: savedContextId }
|
|
26
26
|
}
|
|
27
27
|
|
package/graphql/subscriptions.js
CHANGED
|
@@ -4,14 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
const pubsub = require("./pubsub");
|
|
6
6
|
const { withFilter } = require("graphql-subscriptions");
|
|
7
|
+
const { requestState } = require("./requestState");
|
|
7
8
|
|
|
8
9
|
const subscriptions = {
|
|
9
10
|
requestProgress: {
|
|
10
11
|
subscribe: withFilter(
|
|
11
|
-
() =>
|
|
12
|
+
(_, args, __, info) => {
|
|
13
|
+
const { requestIds } = args;
|
|
14
|
+
for (const requestId of requestIds) {
|
|
15
|
+
if (!requestState[requestId]) {
|
|
16
|
+
console.log(`requestProgress, requestId: ${requestId} not found`);
|
|
17
|
+
} else {
|
|
18
|
+
console.log(`starting async requestProgress, requestId: ${requestId}`);
|
|
19
|
+
const { resolver, args } = requestState[requestId];
|
|
20
|
+
resolver(args);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return pubsub.asyncIterator(['REQUEST_PROGRESS'])
|
|
24
|
+
},
|
|
12
25
|
(payload, variables) => {
|
|
13
26
|
return (
|
|
14
|
-
payload.requestProgress.requestId
|
|
27
|
+
variables.requestIds.includes(payload.requestProgress.requestId)
|
|
15
28
|
);
|
|
16
29
|
},
|
|
17
30
|
),
|
package/graphql/typeDef.js
CHANGED
|
@@ -2,51 +2,60 @@ const GRAPHQL_TYPE_MAP = {
|
|
|
2
2
|
boolean: 'Boolean',
|
|
3
3
|
string: 'String',
|
|
4
4
|
number: 'Int',
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const typeDef = (pathway) => {
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const typeDef = (pathway) => {
|
|
9
8
|
const { name, objName, defaultInputParameters, inputParameters, format } = pathway;
|
|
10
|
-
|
|
9
|
+
|
|
11
10
|
const fields = format ? format.match(/\b(\w+)\b/g) : null;
|
|
12
|
-
const fieldsStr = !fields ? `` : fields.map(f => `${f}: String`).join('\n ');
|
|
13
|
-
|
|
11
|
+
const fieldsStr = !fields ? `` : fields.map((f) => `${f}: String`).join('\n ');
|
|
12
|
+
|
|
14
13
|
const typeName = fields ? `${objName}Result` : `String`;
|
|
15
14
|
const messageType = `input Message { role: String, content: String }`;
|
|
16
|
-
|
|
15
|
+
|
|
17
16
|
const type = fields ? `type ${typeName} {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
${fieldsStr}
|
|
18
|
+
}` : ``;
|
|
19
|
+
|
|
21
20
|
const resultStr = pathway.list ? `[${typeName}]` : typeName;
|
|
22
|
-
|
|
21
|
+
|
|
23
22
|
const responseType = `type ${objName} {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}`;
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
debug: String
|
|
24
|
+
result: ${resultStr}
|
|
25
|
+
previousResult: String
|
|
26
|
+
warnings: [String]
|
|
27
|
+
contextId: String
|
|
28
|
+
}`;
|
|
29
|
+
|
|
32
30
|
const params = { ...defaultInputParameters, ...inputParameters };
|
|
33
|
-
|
|
34
|
-
const paramsStr = Object.entries(params)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
31
|
+
|
|
32
|
+
const paramsStr = Object.entries(params)
|
|
33
|
+
.map(([key, value]) => {
|
|
34
|
+
if (typeof value === 'object' && Array.isArray(value)) {
|
|
35
|
+
return `${key}: [Message] = []`;
|
|
36
|
+
} else {
|
|
37
|
+
return `${key}: ${GRAPHQL_TYPE_MAP[typeof value]} = ${
|
|
38
|
+
typeof value === 'string' ? `"${value}"` : value
|
|
39
|
+
}`;
|
|
41
40
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
})
|
|
42
|
+
.join('\n');
|
|
43
|
+
|
|
44
|
+
const restDefinition = Object.entries(params).map(([key, value]) => {
|
|
45
|
+
return {
|
|
46
|
+
name: key,
|
|
47
|
+
type: `${GRAPHQL_TYPE_MAP[typeof value]}${typeof value === 'object' && Array.isArray(value) ? '[]' : ''}`,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const gqlDefinition = `${messageType}\n\n${type}\n\n${responseType}\n\nextend type Query {${name}(${paramsStr}): ${objName}}`;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
gqlDefinition,
|
|
55
|
+
restDefinition,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
51
60
|
typeDef,
|
|
52
|
-
}
|
|
61
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const ffmpeg = require('fluent-ffmpeg');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { v4: uuidv4 } = require('uuid');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const util = require('util');
|
|
7
|
+
const ffmpegProbe = util.promisify(ffmpeg.ffprobe);
|
|
8
|
+
const pipeline = util.promisify(require('stream').pipeline);
|
|
9
|
+
const ytdl = require('ytdl-core');
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async function processChunk(inputPath, outputFileName, start, duration) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
ffmpeg(inputPath)
|
|
15
|
+
.seekInput(start)
|
|
16
|
+
.duration(duration)
|
|
17
|
+
.on('start', (cmd) => {
|
|
18
|
+
console.log(`Started FFmpeg with command: ${cmd}`);
|
|
19
|
+
})
|
|
20
|
+
.on('error', (err) => {
|
|
21
|
+
console.error(`Error occurred while processing chunk:`, err);
|
|
22
|
+
reject(err);
|
|
23
|
+
})
|
|
24
|
+
.on('end', () => {
|
|
25
|
+
console.log(`Finished processing chunk`);
|
|
26
|
+
resolve(outputFileName);
|
|
27
|
+
})
|
|
28
|
+
.save(outputFileName);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const generateUniqueFolderName = () => {
|
|
33
|
+
const uniqueFolderName = uuidv4();
|
|
34
|
+
const tempFolderPath = os.tmpdir(); // Get the system's temporary folder
|
|
35
|
+
const uniqueOutputPath = path.join(tempFolderPath, uniqueFolderName);
|
|
36
|
+
return uniqueOutputPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const generateUniqueTempFileName = () => {
|
|
40
|
+
return path.join(os.tmpdir(), uuidv4());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function splitMediaFile(inputPath, chunkDurationInSeconds = 600) {
|
|
44
|
+
try {
|
|
45
|
+
const metadata = await ffmpegProbe(inputPath);
|
|
46
|
+
const duration = metadata.format.duration;
|
|
47
|
+
const numChunks = Math.ceil((duration - 1) / chunkDurationInSeconds);
|
|
48
|
+
|
|
49
|
+
const chunkPromises = [];
|
|
50
|
+
|
|
51
|
+
const uniqueOutputPath = generateUniqueFolderName();
|
|
52
|
+
|
|
53
|
+
// Create unique folder
|
|
54
|
+
fs.mkdirSync(uniqueOutputPath, { recursive: true });
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < numChunks; i++) {
|
|
58
|
+
const outputFileName = path.join(
|
|
59
|
+
uniqueOutputPath,
|
|
60
|
+
`chunk-${i + 1}-${path.basename(inputPath)}`
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const chunkPromise = processChunk(
|
|
64
|
+
inputPath,
|
|
65
|
+
outputFileName,
|
|
66
|
+
i * chunkDurationInSeconds,
|
|
67
|
+
chunkDurationInSeconds
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
chunkPromises.push(chunkPromise);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const chunkedFiles = await Promise.all(chunkPromises);
|
|
74
|
+
console.log('All chunks processed. Chunked file names:', chunkedFiles);
|
|
75
|
+
return { chunks: chunkedFiles, folder: uniqueOutputPath }
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('Error occurred during the splitting process:', err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function deleteTempPath(path) {
|
|
82
|
+
try {
|
|
83
|
+
if (!path) return;
|
|
84
|
+
const stats = fs.statSync(path);
|
|
85
|
+
if (stats.isFile()) {
|
|
86
|
+
fs.unlinkSync(path);
|
|
87
|
+
console.log(`Temporary file ${path} deleted successfully.`);
|
|
88
|
+
} else if (stats.isDirectory()) {
|
|
89
|
+
fs.rmdirSync(path, { recursive: true });
|
|
90
|
+
console.log(`Temporary folder ${path} and its contents deleted successfully.`);
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error('Error occurred while deleting the temporary path:', err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
function isValidYoutubeUrl(url) {
|
|
99
|
+
const regex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/;
|
|
100
|
+
return regex.test(url);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function convertYoutubeToMp3Stream(video) {
|
|
104
|
+
// Configure ffmpeg to convert the video to mp3
|
|
105
|
+
const mp3Stream = ffmpeg(video)
|
|
106
|
+
.withAudioCodec('libmp3lame')
|
|
107
|
+
.toFormat('mp3')
|
|
108
|
+
.on('error', (err) => {
|
|
109
|
+
console.error(`An error occurred during conversion: ${err.message}`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return mp3Stream;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function pipeStreamToFile(stream, filePath) {
|
|
116
|
+
try {
|
|
117
|
+
await pipeline(stream, fs.createWriteStream(filePath));
|
|
118
|
+
console.log('Stream piped to file successfully.');
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(`Error piping stream to file: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
const processYoutubeUrl = async (url) => {
|
|
126
|
+
const info = await ytdl.getInfo(url);
|
|
127
|
+
const audioFormat = ytdl.chooseFormat(info.formats, { quality: 'highestaudio' });
|
|
128
|
+
|
|
129
|
+
if (!audioFormat) {
|
|
130
|
+
throw new Error('No suitable audio format found');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const stream = ytdl.downloadFromInfo(info, { format: audioFormat });
|
|
134
|
+
|
|
135
|
+
const mp3Stream = convertYoutubeToMp3Stream(stream);
|
|
136
|
+
const outputFileName = path.join(os.tmpdir(), `${uuidv4()}.mp3`);
|
|
137
|
+
await pipeStreamToFile(mp3Stream, outputFileName); // You can also pipe the stream to a file
|
|
138
|
+
return outputFileName;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function deleteFile(filePath) {
|
|
142
|
+
try {
|
|
143
|
+
fs.unlinkSync(filePath);
|
|
144
|
+
console.log(`File ${filePath} cleaned successfully.`);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(`Error deleting file ${filePath}:`, error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
splitMediaFile, deleteTempPath, processYoutubeUrl, isValidYoutubeUrl
|
|
152
|
+
};
|
package/lib/request.js
CHANGED
|
@@ -1,20 +1,67 @@
|
|
|
1
|
-
const axios = require('axios');
|
|
2
1
|
const Bottleneck = require("bottleneck/es5");
|
|
2
|
+
const RequestMonitor = require('./requestMonitor');
|
|
3
|
+
const { config } = require('../config');
|
|
4
|
+
let axios = require('axios');
|
|
5
|
+
|
|
6
|
+
if (config.get('enableCache')) {
|
|
7
|
+
// Setup cache
|
|
8
|
+
const { setupCache } = require('axios-cache-interceptor');
|
|
9
|
+
axios = setupCache(axios, {
|
|
10
|
+
// enable cache for all requests by default
|
|
11
|
+
methods: ['get', 'post', 'put', 'delete', 'patch'],
|
|
12
|
+
interpretHeader: false,
|
|
13
|
+
ttl: 1000 * 60 * 60 * 24 * 7, // 7 days
|
|
14
|
+
});
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
const limiters = {};
|
|
18
|
+
const monitors = {};
|
|
5
19
|
|
|
6
20
|
const buildLimiters = (config) => {
|
|
7
21
|
console.log('Building limiters...');
|
|
8
22
|
for (const [name, model] of Object.entries(config.get('models'))) {
|
|
23
|
+
const rps = model.requestsPerSecond ?? 100;
|
|
9
24
|
limiters[name] = new Bottleneck({
|
|
10
|
-
minTime: 1000 /
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
minTime: 1000 / rps,
|
|
26
|
+
maxConcurrent: rps,
|
|
27
|
+
reservoir: rps, // Number of tokens available initially
|
|
28
|
+
reservoirRefreshAmount: rps, // Number of tokens added per interval
|
|
29
|
+
reservoirRefreshInterval: 1000, // Interval in milliseconds
|
|
30
|
+
});
|
|
31
|
+
monitors[name] = new RequestMonitor();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setInterval(() => {
|
|
36
|
+
const monitorKeys = Object.keys(monitors);
|
|
37
|
+
|
|
38
|
+
// Skip logging if the monitors object does not exist or is empty
|
|
39
|
+
if (!monitorKeys || monitorKeys.length === 0) {
|
|
40
|
+
return;
|
|
13
41
|
}
|
|
42
|
+
|
|
43
|
+
monitorKeys.forEach((monitorName) => {
|
|
44
|
+
const monitor = monitors[monitorName];
|
|
45
|
+
const callRate = monitor.getPeakCallRate();
|
|
46
|
+
const error429Rate = monitor.getError429Rate();
|
|
47
|
+
if (callRate > 0) {
|
|
48
|
+
console.log('------------------------');
|
|
49
|
+
console.log(`${monitorName} Call rate: ${callRate} calls/sec, 429 errors: ${error429Rate * 100}%`);
|
|
50
|
+
console.log('------------------------');
|
|
51
|
+
// Reset the rate monitor to start a new monitoring interval.
|
|
52
|
+
monitor.reset();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}, 10000); // Log rates every 10 seconds (10000 ms).
|
|
56
|
+
|
|
57
|
+
const postWithMonitor = async (model, url, data, axiosConfigObj) => {
|
|
58
|
+
const monitor = monitors[model];
|
|
59
|
+
monitor.incrementCallCount();
|
|
60
|
+
return axios.post(url, data, axiosConfigObj);
|
|
14
61
|
}
|
|
15
62
|
|
|
16
63
|
const MAX_RETRY = 10;
|
|
17
|
-
const postRequest = async ({ url, data, params, headers }, model) => {
|
|
64
|
+
const postRequest = async ({ url, data, params, headers, cache }, model) => {
|
|
18
65
|
let retry = 0;
|
|
19
66
|
const errors = []
|
|
20
67
|
for (let i = 0; i < MAX_RETRY; i++) {
|
|
@@ -22,13 +69,20 @@ const postRequest = async ({ url, data, params, headers }, model) => {
|
|
|
22
69
|
if (i > 0) {
|
|
23
70
|
console.log(`Retrying request #retry ${i}: ${JSON.stringify(data)}...`);
|
|
24
71
|
await new Promise(r => setTimeout(r, 200 * Math.pow(2, i))); // exponential backoff
|
|
25
|
-
}
|
|
72
|
+
}
|
|
26
73
|
if (!limiters[model]) {
|
|
27
74
|
throw new Error(`No limiter for model ${model}!`);
|
|
28
75
|
}
|
|
29
|
-
|
|
76
|
+
const axiosConfigObj = { params, headers, cache };
|
|
77
|
+
if (params.stream || data.stream) {
|
|
78
|
+
axiosConfigObj.responseType = 'stream';
|
|
79
|
+
}
|
|
80
|
+
return await limiters[model].schedule(() => postWithMonitor(model, url, data, axiosConfigObj));
|
|
30
81
|
} catch (e) {
|
|
31
82
|
console.error(`Failed request with data ${JSON.stringify(data)}: ${e}`);
|
|
83
|
+
if (e.response.status === 429) {
|
|
84
|
+
monitors[model].incrementError429Count();
|
|
85
|
+
}
|
|
32
86
|
errors.push(e);
|
|
33
87
|
}
|
|
34
88
|
}
|
|
@@ -37,7 +91,10 @@ const postRequest = async ({ url, data, params, headers }, model) => {
|
|
|
37
91
|
|
|
38
92
|
const request = async (params, model) => {
|
|
39
93
|
const response = await postRequest(params, model);
|
|
40
|
-
const { error, data } = response;
|
|
94
|
+
const { error, data, cached } = response;
|
|
95
|
+
if (cached) {
|
|
96
|
+
console.info('/Request served with cached response.');
|
|
97
|
+
}
|
|
41
98
|
if (error && error.length > 0) {
|
|
42
99
|
const lastError = error[error.length - 1];
|
|
43
100
|
return { error: lastError.toJSON() ?? lastError ?? error };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
class RequestMonitor {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.callCount = 0;
|
|
4
|
+
this.peakCallRate = 0;
|
|
5
|
+
this.error429Count = 0;
|
|
6
|
+
this.startTime = new Date();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
incrementCallCount() {
|
|
10
|
+
this.callCount++;
|
|
11
|
+
if (this.getCallRate() > this.peakCallRate) {
|
|
12
|
+
this.peakCallRate = this.getCallRate();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
incrementError429Count() {
|
|
17
|
+
this.error429Count++;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getCallRate() {
|
|
21
|
+
const currentTime = new Date();
|
|
22
|
+
const timeElapsed = (currentTime - this.startTime) / 1000; // time elapsed in seconds
|
|
23
|
+
return timeElapsed < 1 ? this.callCount : this.callCount / timeElapsed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getPeakCallRate() {
|
|
27
|
+
return this.peakCallRate;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getError429Rate() {
|
|
31
|
+
return this.error429Count / this.callCount;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
reset() {
|
|
35
|
+
this.callCount = 0;
|
|
36
|
+
this.error429Count = 0;
|
|
37
|
+
this.peakCallRate = 0;
|
|
38
|
+
this.startTime = new Date();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = RequestMonitor;
|
|
43
|
+
|
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aj-archipelago/cortex",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.7",
|
|
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
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/aj-archipelago/cortex.git"
|
|
8
8
|
},
|
|
9
9
|
"keywords": [
|
|
10
10
|
"cortex",
|
|
11
|
-
"
|
|
11
|
+
"AI",
|
|
12
|
+
"prompt engineering",
|
|
13
|
+
"LLM",
|
|
14
|
+
"OpenAI",
|
|
15
|
+
"Azure",
|
|
16
|
+
"GPT-3",
|
|
17
|
+
"GPT-4",
|
|
18
|
+
"chatGPT",
|
|
19
|
+
"GraphQL"
|
|
12
20
|
],
|
|
13
21
|
"main": "index.js",
|
|
14
22
|
"scripts": {
|
|
@@ -22,22 +30,26 @@
|
|
|
22
30
|
"@apollo/utils.keyvadapter": "^1.1.2",
|
|
23
31
|
"@graphql-tools/schema": "^9.0.12",
|
|
24
32
|
"@keyv/redis": "^2.5.4",
|
|
25
|
-
"apollo-server": "^3.
|
|
33
|
+
"apollo-server": "^3.12.0",
|
|
26
34
|
"apollo-server-core": "^3.11.1",
|
|
27
35
|
"apollo-server-express": "^3.11.1",
|
|
28
36
|
"apollo-server-plugin-response-cache": "^3.8.1",
|
|
29
|
-
"axios": "^1.
|
|
37
|
+
"axios": "^1.3.4",
|
|
38
|
+
"axios-cache-interceptor": "^1.0.1",
|
|
30
39
|
"bottleneck": "^2.19.5",
|
|
31
40
|
"compromise": "^14.8.1",
|
|
32
41
|
"compromise-paragraphs": "^0.1.0",
|
|
33
42
|
"convict": "^6.2.3",
|
|
43
|
+
"fluent-ffmpeg": "^2.1.2",
|
|
44
|
+
"form-data": "^4.0.0",
|
|
34
45
|
"gpt-3-encoder": "^1.1.4",
|
|
35
46
|
"graphql": "^16.6.0",
|
|
36
47
|
"graphql-subscriptions": "^2.0.0",
|
|
37
48
|
"graphql-ws": "^5.11.2",
|
|
38
49
|
"handlebars": "^4.7.7",
|
|
39
50
|
"keyv": "^4.5.2",
|
|
40
|
-
"ws": "^8.12.0"
|
|
51
|
+
"ws": "^8.12.0",
|
|
52
|
+
"ytdl-core": "^4.11.2"
|
|
41
53
|
},
|
|
42
54
|
"devDependencies": {
|
|
43
55
|
"dotenv": "^16.0.3",
|
package/pathways/basePathway.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { parseResponse } = require("../graphql/parser");
|
|
2
1
|
const { rootResolver, resolver } = require("../graphql/resolver");
|
|
3
2
|
const { typeDef } = require('../graphql/typeDef')
|
|
4
3
|
|
|
@@ -7,9 +6,9 @@ module.exports = {
|
|
|
7
6
|
prompt: `{{text}}`,
|
|
8
7
|
defaultInputParameters: {
|
|
9
8
|
text: ``,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
async: false, // switch to enable async mode
|
|
10
|
+
contextId: ``, // used to identify the context of the request,
|
|
11
|
+
stream: false, // switch to enable stream mode
|
|
13
12
|
},
|
|
14
13
|
inputParameters: {},
|
|
15
14
|
typeDef,
|
package/pathways/bias.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// bias.js
|
|
2
|
+
// Objectivity analysis of text
|
|
3
|
+
// This module exports a prompt that analyzes the given text and determines if it's written objectively. It also provides a detailed explanation of the decision.
|
|
4
|
+
|
|
1
5
|
module.exports = {
|
|
6
|
+
// Uncomment the following line to enable caching for this prompt, if desired.
|
|
7
|
+
// enableCache: true,
|
|
8
|
+
|
|
2
9
|
prompt: `{{text}}\n\nIs the above text written objectively? Why or why not, explain with details:\n`
|
|
3
10
|
}
|
package/pathways/chat.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// chat.js
|
|
2
|
+
// Simple context-aware chat bot
|
|
3
|
+
// This is a two prompt implementation of a context aware chat bot. The first prompt generates content that will be stored in the previousResult variable and will be returned to the client. In the optimum implementation, the client will then update their chatContext variable for the next call. The second prompt actually responds to the user. The second prompt *could* use previousResult instead of chatContext, but in this situation previousResult will also include the current turn of the conversation to which it is responding. That can get a little confusing as it tends to overemphasize the current turn in the response.
|
|
4
|
+
|
|
2
5
|
module.exports = {
|
|
3
6
|
prompt:
|
|
4
7
|
[
|
package/pathways/complete.js
CHANGED