@aj-archipelago/cortex 1.0.25 → 1.1.0-beta.0
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/.eslintrc +1 -1
- package/config.js +17 -7
- package/lib/logger.js +29 -0
- package/lib/redisSubscription.js +92 -20
- package/lib/request.js +22 -21
- package/package.json +3 -2
- package/server/graphql.js +5 -4
- package/server/pathwayResolver.js +14 -21
- package/server/plugins/azureCognitivePlugin.js +5 -4
- package/server/plugins/azureTranslatePlugin.js +2 -2
- package/server/plugins/localModelPlugin.js +3 -3
- package/server/plugins/modelPlugin.js +15 -6
- package/server/plugins/openAiChatPlugin.js +16 -4
- package/server/plugins/openAiCompletionPlugin.js +9 -3
- package/server/plugins/openAiDallE3Plugin.js +6 -7
- package/server/plugins/openAiImagePlugin.js +4 -7
- package/server/plugins/openAiWhisperPlugin.js +16 -16
- package/server/plugins/palmChatPlugin.js +14 -6
- package/server/plugins/palmCompletionPlugin.js +10 -3
- package/server/pubsub.js +1 -0
- package/server/rest.js +7 -6
- package/server/subscriptions.js +3 -15
- package/tests/palmChatPlugin.test.js +0 -38
- package/tests/palmCompletionPlugin.test.js +0 -30
package/.eslintrc
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
],
|
|
17
17
|
"rules": {
|
|
18
18
|
"import/no-unresolved": "error",
|
|
19
|
-
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}],
|
|
19
|
+
"import/no-extraneous-dependencies": ["error", {"devDependencies": true, "dependencies": true}],
|
|
20
20
|
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
|
21
21
|
},
|
|
22
22
|
"settings": {
|
package/config.js
CHANGED
|
@@ -4,11 +4,17 @@ import HandleBars from './lib/handleBars.js';
|
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
6
6
|
import GcpAuthTokenHelper from './lib/gcpAuthTokenHelper.js';
|
|
7
|
+
import logger from './lib/logger.js';
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
|
|
10
11
|
// Schema for config
|
|
11
12
|
var config = convict({
|
|
13
|
+
env: {
|
|
14
|
+
format: String,
|
|
15
|
+
default: 'development',
|
|
16
|
+
env: 'NODE_ENV'
|
|
17
|
+
},
|
|
12
18
|
cortexId: {
|
|
13
19
|
format: String,
|
|
14
20
|
default: 'local',
|
|
@@ -195,14 +201,16 @@ const configFile = config.get('cortexConfigFile');
|
|
|
195
201
|
|
|
196
202
|
// Load config file
|
|
197
203
|
if (configFile && fs.existsSync(configFile)) {
|
|
198
|
-
|
|
204
|
+
logger.info(`Loading config from ${configFile}`);
|
|
199
205
|
config.loadFile(configFile);
|
|
200
206
|
} else {
|
|
201
207
|
const openaiApiKey = config.get('openaiApiKey');
|
|
202
208
|
if (!openaiApiKey) {
|
|
203
|
-
|
|
209
|
+
const errorString = 'No config file or api key specified. Please set the OPENAI_API_KEY to use OAI or use CORTEX_CONFIG_FILE environment variable to point at the Cortex configuration for your project.';
|
|
210
|
+
logger.error(errorString);
|
|
211
|
+
throw new Error(errorString);
|
|
204
212
|
} else {
|
|
205
|
-
|
|
213
|
+
logger.info(`Using default model with OPENAI_API_KEY environment variable`)
|
|
206
214
|
}
|
|
207
215
|
}
|
|
208
216
|
|
|
@@ -223,12 +231,12 @@ const buildPathways = async (config) => {
|
|
|
223
231
|
const basePathway = await import(basePathwayURL).then(module => module.default);
|
|
224
232
|
|
|
225
233
|
// Load core pathways, default from the Cortex package
|
|
226
|
-
|
|
234
|
+
logger.info(`Loading core pathways from ${corePathwaysPath}`)
|
|
227
235
|
let loadedPathways = await import(`${corePathwaysURL}/index.js`).then(module => module);
|
|
228
236
|
|
|
229
237
|
// Load custom pathways and override core pathways if same
|
|
230
238
|
if (pathwaysPath && fs.existsSync(pathwaysPath)) {
|
|
231
|
-
|
|
239
|
+
logger.info(`Loading custom pathways from ${pathwaysPath}`)
|
|
232
240
|
const customPathways = await import(`${pathwaysURL}/index.js`).then(module => module);
|
|
233
241
|
loadedPathways = { ...loadedPathways, ...customPathways };
|
|
234
242
|
}
|
|
@@ -263,12 +271,14 @@ const buildModels = (config) => {
|
|
|
263
271
|
|
|
264
272
|
// Check that models are specified, Cortex cannot run without a model
|
|
265
273
|
if (Object.keys(config.get('models')).length <= 0) {
|
|
266
|
-
|
|
274
|
+
const errorString = 'No models specified! Please set the models in your config file or via CORTEX_MODELS environment variable to point at the models for your project.';
|
|
275
|
+
logger.error(errorString);
|
|
276
|
+
throw new Error(errorString);
|
|
267
277
|
}
|
|
268
278
|
|
|
269
279
|
// Set default model name to the first model in the config in case no default is specified
|
|
270
280
|
if (!config.get('defaultModelName')) {
|
|
271
|
-
|
|
281
|
+
logger.warn('No default model specified, using first model as default.');
|
|
272
282
|
config.load({ defaultModelName: Object.keys(config.get('models'))[0] });
|
|
273
283
|
}
|
|
274
284
|
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// logger.js
|
|
2
|
+
import winston from 'winston';
|
|
3
|
+
|
|
4
|
+
const format = winston.format.combine(
|
|
5
|
+
//winston.format.timestamp(),
|
|
6
|
+
winston.format.colorize({ all: true }),
|
|
7
|
+
winston.format.simple()
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const transports = [
|
|
11
|
+
new winston.transports.Console({ format })
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const logger = winston.createLogger({
|
|
15
|
+
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
|
16
|
+
format: format,
|
|
17
|
+
transports: transports
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
winston.addColors({
|
|
21
|
+
debug: 'green',
|
|
22
|
+
verbose: 'blue',
|
|
23
|
+
http: 'gray',
|
|
24
|
+
info: 'cyan',
|
|
25
|
+
warn: 'yellow',
|
|
26
|
+
error: 'red'
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export default logger;
|
package/lib/redisSubscription.js
CHANGED
|
@@ -1,39 +1,47 @@
|
|
|
1
1
|
import Redis from 'ioredis';
|
|
2
2
|
import { config } from '../config.js';
|
|
3
3
|
import pubsub from '../server/pubsub.js';
|
|
4
|
+
import { requestState } from '../server/requestState.js';
|
|
5
|
+
import logger from '../lib/logger.js';
|
|
4
6
|
|
|
5
7
|
const connectionString = config.get('storageConnectionString');
|
|
6
|
-
const
|
|
8
|
+
const channels = ['requestProgress', 'requestProgressSubscriptions'];
|
|
7
9
|
let client;
|
|
8
10
|
|
|
9
11
|
if (connectionString) {
|
|
10
|
-
|
|
12
|
+
logger.info(`Using Redis subscription for channel(s) ${channels.join(', ')}`);
|
|
11
13
|
try {
|
|
12
14
|
client = connectionString && new Redis(connectionString);
|
|
13
15
|
} catch (error) {
|
|
14
|
-
|
|
16
|
+
logger.error(`Redis connection error: ${JSON.stringify(error)}`);
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
if (client) {
|
|
18
|
-
const channel = 'requestProgress';
|
|
19
20
|
|
|
20
21
|
client.on('error', (error) => {
|
|
21
|
-
|
|
22
|
+
logger.error(`Redis client error: ${JSON.stringify(error)}`);
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
client.on('connect', () => {
|
|
25
|
-
client.subscribe(
|
|
26
|
+
client.subscribe('requestProgress', (error) => {
|
|
26
27
|
if (error) {
|
|
27
|
-
|
|
28
|
+
logger.error(`Error subscribing to redis channel requestProgress: ${JSON.stringify(error)}`);
|
|
28
29
|
} else {
|
|
29
|
-
|
|
30
|
+
logger.info(`Subscribed to channel requestProgress`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
client.subscribe('requestProgressSubscriptions', (error) => {
|
|
34
|
+
if (error) {
|
|
35
|
+
logger.error(`Error subscribing to redis channel requestProgressSubscriptions: ${JSON.stringify(error)}`);
|
|
36
|
+
} else {
|
|
37
|
+
logger.info(`Subscribed to channel requestProgressSubscriptions`);
|
|
30
38
|
}
|
|
31
39
|
});
|
|
32
40
|
});
|
|
33
41
|
|
|
34
42
|
client.on('message', (channel, message) => {
|
|
35
43
|
if (channel === 'requestProgress') {
|
|
36
|
-
|
|
44
|
+
logger.debug(`Received message from ${channel}: ${message}`);
|
|
37
45
|
let parsedMessage;
|
|
38
46
|
|
|
39
47
|
try {
|
|
@@ -42,22 +50,86 @@ if (connectionString) {
|
|
|
42
50
|
parsedMessage = message;
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
pubsubHandleMessage(parsedMessage);
|
|
54
|
+
} else {
|
|
55
|
+
if (channel === 'requestProgressSubscriptions') {
|
|
56
|
+
logger.debug(`Received message from ${channel}: ${message}`);
|
|
57
|
+
let parsedMessage;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
parsedMessage = JSON.parse(message);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
parsedMessage = message;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
handleSubscription(parsedMessage);
|
|
66
|
+
}
|
|
46
67
|
}
|
|
47
68
|
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
48
71
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
|
|
73
|
+
let publisherClient;
|
|
74
|
+
|
|
75
|
+
if (connectionString) {
|
|
76
|
+
logger.info(`Using Redis publish for channel(s) ${channels.join(', ')}`);
|
|
77
|
+
publisherClient = Redis.createClient(connectionString);
|
|
78
|
+
} else {
|
|
79
|
+
logger.info(`Using pubsub publish for channel ${channels[0]}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function publishRequestProgress(data) {
|
|
83
|
+
if (publisherClient) {
|
|
84
|
+
try {
|
|
85
|
+
const message = JSON.stringify(data);
|
|
86
|
+
logger.debug(`Publishing message ${message} to channel ${channels[0]}`);
|
|
87
|
+
await publisherClient.publish(channels[0], message);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.error(`Error publishing message: ${JSON.stringify(error)}`);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
pubsubHandleMessage(data);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function publishRequestProgressSubscription(data) {
|
|
97
|
+
if (publisherClient) {
|
|
98
|
+
try {
|
|
99
|
+
const message = JSON.stringify(data);
|
|
100
|
+
logger.debug(`Publishing message ${message} to channel ${channels[1]}`);
|
|
101
|
+
await publisherClient.publish(channels[1], message);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.error(`Error publishing message: ${JSON.stringify(error)}`);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
handleSubscription(data);
|
|
58
107
|
}
|
|
59
108
|
}
|
|
60
109
|
|
|
110
|
+
function pubsubHandleMessage(data){
|
|
111
|
+
const message = JSON.stringify(data);
|
|
112
|
+
logger.debug(`Publishing message to pubsub: ${message}`);
|
|
113
|
+
try {
|
|
114
|
+
pubsub.publish('REQUEST_PROGRESS', { requestProgress: data });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger.error(`Error publishing data to pubsub: ${JSON.stringify(error)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function handleSubscription(data){
|
|
121
|
+
const requestIds = data;
|
|
122
|
+
for (const requestId of requestIds) {
|
|
123
|
+
if (requestState[requestId] && !requestState[requestId].started) {
|
|
124
|
+
requestState[requestId].started = true;
|
|
125
|
+
logger.info(`Subscription starting async requestProgress, requestId: ${requestId}`);
|
|
126
|
+
const { resolver, args } = requestState[requestId];
|
|
127
|
+
resolver(args);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
61
133
|
export {
|
|
62
|
-
client as subscriptionClient,
|
|
134
|
+
client as subscriptionClient, publishRequestProgress, publishRequestProgressSubscription
|
|
63
135
|
};
|
package/lib/request.js
CHANGED
|
@@ -4,13 +4,14 @@ import { config } from '../config.js';
|
|
|
4
4
|
import axios from 'axios';
|
|
5
5
|
import { setupCache } from 'axios-cache-interceptor';
|
|
6
6
|
import Redis from 'ioredis';
|
|
7
|
+
import logger from './logger.js';
|
|
7
8
|
|
|
8
9
|
const connectionString = config.get('storageConnectionString');
|
|
9
10
|
|
|
10
11
|
if (!connectionString) {
|
|
11
|
-
|
|
12
|
+
logger.info('No STORAGE_CONNECTION_STRING found in environment. Redis features (caching, pubsub, clustered limiters) disabled.')
|
|
12
13
|
} else {
|
|
13
|
-
|
|
14
|
+
logger.info('Using Redis connection specified in STORAGE_CONNECTION_STRING.');
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
let client;
|
|
@@ -19,7 +20,7 @@ if (connectionString) {
|
|
|
19
20
|
try {
|
|
20
21
|
client = new Redis(connectionString);
|
|
21
22
|
} catch (error) {
|
|
22
|
-
|
|
23
|
+
logger.error(`Redis connection error: ${error}`);
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -30,7 +31,7 @@ const limiters = {};
|
|
|
30
31
|
const monitors = {};
|
|
31
32
|
|
|
32
33
|
const buildLimiters = (config) => {
|
|
33
|
-
|
|
34
|
+
logger.info(`Building ${connection ? 'Redis clustered' : 'local'} model rate limiters for ${cortexId}...`);
|
|
34
35
|
for (const [name, model] of Object.entries(config.get('models'))) {
|
|
35
36
|
const rps = model.requestsPerSecond ?? 100;
|
|
36
37
|
let limiterOptions = {
|
|
@@ -50,7 +51,7 @@ const buildLimiters = (config) => {
|
|
|
50
51
|
|
|
51
52
|
limiters[name] = new Bottleneck(limiterOptions);
|
|
52
53
|
limiters[name].on('error', (err) => {
|
|
53
|
-
|
|
54
|
+
logger.error(`Limiter error for ${cortexId}-${name}: ${err}`);
|
|
54
55
|
});
|
|
55
56
|
monitors[name] = new RequestMonitor();
|
|
56
57
|
}
|
|
@@ -81,9 +82,9 @@ setInterval(() => {
|
|
|
81
82
|
const callRate = monitor.getPeakCallRate();
|
|
82
83
|
const error429Rate = monitor.getError429Rate();
|
|
83
84
|
if (callRate > 0) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
logger.info('------------------------');
|
|
86
|
+
logger.info(`${monitorName} Call rate: ${callRate} calls/sec, 429 errors: ${error429Rate * 100}%`);
|
|
87
|
+
logger.info('------------------------');
|
|
87
88
|
// Reset the rate monitor to start a new monitoring interval.
|
|
88
89
|
monitor.reset();
|
|
89
90
|
}
|
|
@@ -109,7 +110,7 @@ const postRequest = async ({ url, data, params, headers, cache }, model, request
|
|
|
109
110
|
let duplicateRequestAfter = (pathway?.duplicateRequestAfter || DUPLICATE_REQUEST_AFTER) * 1000;
|
|
110
111
|
|
|
111
112
|
if (enableDuplicateRequests) {
|
|
112
|
-
//
|
|
113
|
+
//logger.info(`>>> [${requestId}] Duplicate requests enabled after ${duplicateRequestAfter / 1000} seconds`);
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
const axiosConfigObj = { params, headers, cache };
|
|
@@ -119,7 +120,7 @@ const postRequest = async ({ url, data, params, headers, cache }, model, request
|
|
|
119
120
|
promises.push(limiters[model].schedule(() => postWithMonitor(model, url, data, axiosConfigObj)));
|
|
120
121
|
} else {
|
|
121
122
|
if (streamRequested) {
|
|
122
|
-
|
|
123
|
+
logger.info(`>>> [${requestId}] ${model} does not support streaming - sending non-streaming request`);
|
|
123
124
|
axiosConfigObj.params.stream = false;
|
|
124
125
|
data.stream = false;
|
|
125
126
|
}
|
|
@@ -144,7 +145,7 @@ const postRequest = async ({ url, data, params, headers, cache }, model, request
|
|
|
144
145
|
axiosConfigObj.headers['X-Cortex-Request-Index'] = index;
|
|
145
146
|
|
|
146
147
|
if (index === 0) {
|
|
147
|
-
//
|
|
148
|
+
//logger.info(`>>> [${requestId}] sending request to ${model} API ${axiosConfigObj.responseType === 'stream' ? 'with streaming' : ''}`);
|
|
148
149
|
} else {
|
|
149
150
|
if (modelProperties.supportsStreaming) {
|
|
150
151
|
axiosConfigObj.responseType = 'stream';
|
|
@@ -152,23 +153,23 @@ const postRequest = async ({ url, data, params, headers, cache }, model, request
|
|
|
152
153
|
}
|
|
153
154
|
const logMessage = `>>> [${requestId}] taking too long - sending duplicate request ${index} to ${model} API ${axiosConfigObj.responseType === 'stream' ? 'with streaming' : ''}`;
|
|
154
155
|
const header = '>'.repeat(logMessage.length);
|
|
155
|
-
|
|
156
|
+
logger.info(`\n${header}\n${logMessage}`);
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
response = await limiters[model].schedule(() => postWithMonitor(model, url, data, axiosConfigObj));
|
|
159
160
|
|
|
160
161
|
if (!controller.signal?.aborted) {
|
|
161
162
|
|
|
162
|
-
//
|
|
163
|
+
//logger.info(`<<< [${requestId}] received response for request ${index}`);
|
|
163
164
|
|
|
164
165
|
if (axiosConfigObj.responseType === 'stream') {
|
|
165
166
|
// Buffering and collecting the stream data
|
|
166
|
-
|
|
167
|
+
logger.info(`<<< [${requestId}] buffering streaming response for request ${index}`);
|
|
167
168
|
response = await new Promise((resolve, reject) => {
|
|
168
169
|
let responseData = '';
|
|
169
170
|
response.data.on('data', (chunk) => {
|
|
170
171
|
responseData += chunk;
|
|
171
|
-
//
|
|
172
|
+
//logger.info(`<<< [${requestId}] received chunk for request ${index}`);
|
|
172
173
|
});
|
|
173
174
|
response.data.on('end', () => {
|
|
174
175
|
response.data = JSON.parse(responseData);
|
|
@@ -186,10 +187,10 @@ const postRequest = async ({ url, data, params, headers, cache }, model, request
|
|
|
186
187
|
|
|
187
188
|
} catch (error) {
|
|
188
189
|
if (error.name === 'AbortError' || error.name === 'CanceledError') {
|
|
189
|
-
//
|
|
190
|
+
//logger.info(`XXX [${requestId}] request ${index} was cancelled`);
|
|
190
191
|
reject(error);
|
|
191
192
|
} else {
|
|
192
|
-
|
|
193
|
+
logger.info(`!!! [${requestId}] request ${index} failed with error: ${error?.response?.data?.error?.message || error}`);
|
|
193
194
|
reject(error);
|
|
194
195
|
}
|
|
195
196
|
} finally {
|
|
@@ -210,14 +211,14 @@ const postRequest = async ({ url, data, params, headers, cache }, model, request
|
|
|
210
211
|
throw new Error(`Received error response: ${response.status}`);
|
|
211
212
|
}
|
|
212
213
|
} catch (error) {
|
|
213
|
-
//
|
|
214
|
+
//logger.error(`!!! [${requestId}] failed request with data ${JSON.stringify(data)}: ${error}`);
|
|
214
215
|
if (error.response) {
|
|
215
216
|
const status = error.response.status;
|
|
216
217
|
if ((status === 429) || (status >= 500 && status < 600)) {
|
|
217
218
|
if (status === 429) {
|
|
218
219
|
monitors[model].incrementError429Count();
|
|
219
220
|
}
|
|
220
|
-
|
|
221
|
+
logger.info(`>>> [${requestId}] retrying request due to ${status} response. Retry count: ${i + 1}`);
|
|
221
222
|
if (i < MAX_RETRY - 1) {
|
|
222
223
|
const backoffTime = 200 * Math.pow(2, i);
|
|
223
224
|
const jitter = backoffTime * 0.2 * Math.random();
|
|
@@ -246,10 +247,10 @@ const request = async (params, model, requestId, pathway) => {
|
|
|
246
247
|
const lastError = error[error.length - 1];
|
|
247
248
|
return { error: lastError.toJSON() ?? lastError ?? error };
|
|
248
249
|
}
|
|
249
|
-
//
|
|
250
|
+
//logger.info(`<<< [${requestId}] response: ${data.choices[0].delta || data.choices[0]}`)
|
|
250
251
|
return data;
|
|
251
252
|
} catch (error) {
|
|
252
|
-
|
|
253
|
+
logger.error(`Error in request: ${error.message || error}`);
|
|
253
254
|
return { error: error };
|
|
254
255
|
}
|
|
255
256
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aj-archipelago/cortex",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.1.0-beta.0",
|
|
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": {
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"langchain": "^0.0.47",
|
|
55
55
|
"subsrt": "^1.1.1",
|
|
56
56
|
"uuid": "^9.0.0",
|
|
57
|
+
"winston": "^3.11.0",
|
|
57
58
|
"ws": "^8.12.0"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
"eslint": "^8.38.0",
|
|
63
64
|
"eslint-plugin-import": "^2.27.5",
|
|
64
65
|
"got": "^13.0.0",
|
|
65
|
-
"sinon": "^
|
|
66
|
+
"sinon": "^17.0.1"
|
|
66
67
|
},
|
|
67
68
|
"publishConfig": {
|
|
68
69
|
"access": "restricted"
|
package/server/graphql.js
CHANGED
|
@@ -22,6 +22,7 @@ import { buildPathways, buildModels } from '../config.js';
|
|
|
22
22
|
import { requestState } from './requestState.js';
|
|
23
23
|
import { buildRestEndpoints } from './rest.js';
|
|
24
24
|
import { startTestServer } from '../tests/server.js'
|
|
25
|
+
import logger from '../lib/logger.js';
|
|
25
26
|
|
|
26
27
|
// Utility functions
|
|
27
28
|
// Server plugins
|
|
@@ -41,7 +42,7 @@ const getPlugins = (config) => {
|
|
|
41
42
|
// TODO: custom cache key:
|
|
42
43
|
// https://www.apollographql.com/docs/apollo-server/performance/cache-backends#implementing-your-own-cache-backend
|
|
43
44
|
plugins.push(responseCachePlugin({ cache }));
|
|
44
|
-
|
|
45
|
+
logger.info('Using Redis for GraphQL cache');
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
return { plugins, cache };
|
|
@@ -146,12 +147,12 @@ const build = async (config) => {
|
|
|
146
147
|
// Respects the keep alive setting in config in case you want to
|
|
147
148
|
// turn it off for deployments that don't route the ping/pong frames
|
|
148
149
|
const keepAlive = config.get('subscriptionKeepAlive');
|
|
149
|
-
|
|
150
|
+
logger.info(`Starting web socket server with subscription keep alive: ${keepAlive}`);
|
|
150
151
|
const serverCleanup = useServer({ schema }, wsServer, keepAlive);
|
|
151
152
|
|
|
152
153
|
const server = new ApolloServer({
|
|
153
154
|
schema,
|
|
154
|
-
introspection:
|
|
155
|
+
introspection: config.get('env') === 'development',
|
|
155
156
|
csrfPrevention: true,
|
|
156
157
|
plugins: plugins.concat([// Proper shutdown for the HTTP server.
|
|
157
158
|
ApolloServerPluginDrainHttpServer({ httpServer }),
|
|
@@ -232,7 +233,7 @@ const build = async (config) => {
|
|
|
232
233
|
|
|
233
234
|
// Now that our HTTP server is fully set up, we can listen to it.
|
|
234
235
|
httpServer.listen(config.get('PORT'), () => {
|
|
235
|
-
|
|
236
|
+
logger.info(`🚀 Server is now running at http://localhost:${config.get('PORT')}/graphql`);
|
|
236
237
|
});
|
|
237
238
|
};
|
|
238
239
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { PathwayPrompter } from './pathwayPrompter.js';
|
|
2
2
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
-
import pubsub from './pubsub.js';
|
|
5
4
|
import { encode } from 'gpt-3-encoder';
|
|
6
5
|
import { getFirstNToken, getLastNToken, getSemanticChunks } from './chunker.js';
|
|
7
6
|
import { PathwayResponseParser } from './pathwayResponseParser.js';
|
|
@@ -9,6 +8,8 @@ import { Prompt } from './prompt.js';
|
|
|
9
8
|
import { getv, setv } from '../lib/keyValueStorageClient.js';
|
|
10
9
|
import { requestState } from './requestState.js';
|
|
11
10
|
import { callPathway } from '../lib/pathwayTools.js';
|
|
11
|
+
import { publishRequestProgress } from '../lib/redisSubscription.js';
|
|
12
|
+
import logger from '../lib/logger.js';
|
|
12
13
|
|
|
13
14
|
const modelTypesExcludedFromProgressUpdates = ['OPENAI-DALLE2', 'OPENAI-DALLE3'];
|
|
14
15
|
|
|
@@ -83,12 +84,10 @@ class PathwayResolver {
|
|
|
83
84
|
|
|
84
85
|
// if model type is OPENAI-IMAGE
|
|
85
86
|
if (!modelTypesExcludedFromProgressUpdates.includes(this.model.type)) {
|
|
86
|
-
|
|
87
|
-
requestProgress: {
|
|
87
|
+
await publishRequestProgress({
|
|
88
88
|
requestId: this.requestId,
|
|
89
89
|
progress: completedCount / totalCount,
|
|
90
90
|
data: JSON.stringify(responseData),
|
|
91
|
-
}
|
|
92
91
|
});
|
|
93
92
|
}
|
|
94
93
|
} else {
|
|
@@ -99,7 +98,7 @@ class PathwayResolver {
|
|
|
99
98
|
|
|
100
99
|
const processData = (data) => {
|
|
101
100
|
try {
|
|
102
|
-
//
|
|
101
|
+
//logger.info(`\n\nReceived stream data for requestId ${this.requestId}: ${data.toString()}`);
|
|
103
102
|
let events = data.toString().split('\n');
|
|
104
103
|
|
|
105
104
|
//events = "data: {\"id\":\"chatcmpl-20bf1895-2fa7-4ef9-abfe-4d142aba5817\",\"object\":\"chat.completion.chunk\",\"created\":1689303423723,\"model\":\"gpt-4\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":{\"error\":{\"message\":\"The server had an error while processing your request. Sorry about that!\",\"type\":\"server_error\",\"param\":null,\"code\":null}}},\"finish_reason\":null}]}\n\n".split("\n");
|
|
@@ -109,7 +108,7 @@ class PathwayResolver {
|
|
|
109
108
|
|
|
110
109
|
// skip empty events
|
|
111
110
|
if (!(event.trim() === '')) {
|
|
112
|
-
//
|
|
111
|
+
//logger.info(`Processing stream event for requestId ${this.requestId}: ${event}`);
|
|
113
112
|
messageBuffer += event.replace(/^data: /, '');
|
|
114
113
|
|
|
115
114
|
const requestProgress = {
|
|
@@ -133,24 +132,22 @@ class PathwayResolver {
|
|
|
133
132
|
const streamError = parsedMessage?.error || parsedMessage?.choices?.[0]?.delta?.content?.error || parsedMessage?.choices?.[0]?.text?.error;
|
|
134
133
|
if (streamError) {
|
|
135
134
|
streamErrorOccurred = true;
|
|
136
|
-
|
|
135
|
+
logger.error(`Stream error: ${streamError.message}`);
|
|
137
136
|
incomingMessage.off('data', processData); // Stop listening to 'data'
|
|
138
137
|
return;
|
|
139
138
|
}
|
|
140
139
|
}
|
|
141
140
|
|
|
142
141
|
try {
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
requestProgress: requestProgress
|
|
146
|
-
});
|
|
142
|
+
//logger.info(`Publishing stream message to requestId ${this.requestId}: ${message}`);
|
|
143
|
+
publishRequestProgress(requestProgress);
|
|
147
144
|
} catch (error) {
|
|
148
|
-
|
|
145
|
+
logger.error(`Could not publish the stream message: "${messageBuffer}", ${error}`);
|
|
149
146
|
}
|
|
150
147
|
}
|
|
151
148
|
}
|
|
152
149
|
} catch (error) {
|
|
153
|
-
|
|
150
|
+
logger.error(`Could not process stream data: ${error}`);
|
|
154
151
|
}
|
|
155
152
|
};
|
|
156
153
|
|
|
@@ -163,25 +160,23 @@ class PathwayResolver {
|
|
|
163
160
|
}
|
|
164
161
|
|
|
165
162
|
} catch (error) {
|
|
166
|
-
|
|
163
|
+
logger.error(`Could not subscribe to stream: ${error}`);
|
|
167
164
|
}
|
|
168
165
|
}
|
|
169
166
|
|
|
170
167
|
if (streamErrorOccurred) {
|
|
171
168
|
attempt++;
|
|
172
|
-
|
|
169
|
+
logger.error(`Stream attempt ${attempt} failed. Retrying...`);
|
|
173
170
|
streamErrorOccurred = false; // Reset the flag for the next attempt
|
|
174
171
|
} else {
|
|
175
172
|
return;
|
|
176
173
|
}
|
|
177
174
|
}
|
|
178
175
|
// if all retries failed, publish the stream end message
|
|
179
|
-
|
|
180
|
-
requestProgress: {
|
|
176
|
+
publishRequestProgress({
|
|
181
177
|
requestId: this.requestId,
|
|
182
178
|
progress: 1,
|
|
183
179
|
data: '[DONE]',
|
|
184
|
-
}
|
|
185
180
|
});
|
|
186
181
|
}
|
|
187
182
|
|
|
@@ -401,11 +396,9 @@ class PathwayResolver {
|
|
|
401
396
|
const { completedCount, totalCount } = requestState[this.requestId];
|
|
402
397
|
|
|
403
398
|
if (completedCount < totalCount) {
|
|
404
|
-
|
|
405
|
-
requestProgress: {
|
|
399
|
+
await publishRequestProgress({
|
|
406
400
|
requestId: this.requestId,
|
|
407
401
|
progress: completedCount / totalCount,
|
|
408
|
-
}
|
|
409
402
|
});
|
|
410
403
|
}
|
|
411
404
|
|
|
@@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { config } from '../../config.js';
|
|
7
7
|
import { axios } from '../../lib/request.js';
|
|
8
|
+
import logger from '../../lib/logger.js';
|
|
8
9
|
|
|
9
10
|
const API_URL = config.get('whisperMediaApiUrl');
|
|
10
11
|
|
|
@@ -24,7 +25,7 @@ class AzureCognitivePlugin extends ModelPlugin {
|
|
|
24
25
|
}
|
|
25
26
|
return JSON.parse(await callPathway(this.config, 'embeddings', { text }))[0];
|
|
26
27
|
}catch(err){
|
|
27
|
-
|
|
28
|
+
logger.error(`Error in calculating input vector for text: ${text}, error: ${err}`);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -134,11 +135,11 @@ class AzureCognitivePlugin extends ModelPlugin {
|
|
|
134
135
|
if (API_URL) {
|
|
135
136
|
//call helper api to mark processing as completed
|
|
136
137
|
const res = await axios.delete(API_URL, { params: { requestId } });
|
|
137
|
-
|
|
138
|
+
logger.info(`Marked request ${requestId} as completed: ${res.data}`);
|
|
138
139
|
return res.data;
|
|
139
140
|
}
|
|
140
141
|
} catch (err) {
|
|
141
|
-
|
|
142
|
+
logger.error(`Error marking request ${requestId} as completed: ${err}`);
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
|
|
@@ -161,7 +162,7 @@ class AzureCognitivePlugin extends ModelPlugin {
|
|
|
161
162
|
const { data } = await axios.get(API_URL, { params: { uri: file, requestId, save: true } });
|
|
162
163
|
url = data[0];
|
|
163
164
|
} catch (error) {
|
|
164
|
-
|
|
165
|
+
logger.error(`Error converting file ${file} to txt: ${error}`);
|
|
165
166
|
await this.markCompletedForCleanUp(requestId);
|
|
166
167
|
throw Error(error?.response?.data || error?.message || error);
|
|
167
168
|
}
|
|
@@ -52,8 +52,8 @@ class AzureTranslatePlugin extends ModelPlugin {
|
|
|
52
52
|
|
|
53
53
|
const modelInput = data[0].Text;
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
logger.debug(`${modelInput}`);
|
|
56
|
+
logger.debug(`${this.parseResponse(responseData)}`);
|
|
57
57
|
|
|
58
58
|
prompt && prompt.debugInfo && (prompt.debugInfo += `\n${JSON.stringify(data)}`);
|
|
59
59
|
}
|
|
@@ -54,16 +54,16 @@ class LocalModelPlugin extends ModelPlugin {
|
|
|
54
54
|
//args.push("--temperature", requestParameters.temperature);
|
|
55
55
|
|
|
56
56
|
try {
|
|
57
|
-
|
|
57
|
+
logger.debug(`Running local model: ${executablePath}, ${args}`);
|
|
58
58
|
const result = execFileSync(executablePath, args, { encoding: 'utf8' });
|
|
59
59
|
// Remove only the first occurrence of requestParameters.prompt from the result
|
|
60
60
|
// Could have used regex here but then would need to escape the prompt
|
|
61
61
|
const parts = result.split(requestParameters.prompt, 2);
|
|
62
62
|
const modifiedResult = parts[0] + parts[1];
|
|
63
|
-
|
|
63
|
+
logger.debug(`Result: ${modifiedResult}`);
|
|
64
64
|
return this.filterFirstResponse(modifiedResult);
|
|
65
65
|
} catch (error) {
|
|
66
|
-
|
|
66
|
+
logger.error(`Error running local model: ${error}`);
|
|
67
67
|
throw error;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -4,6 +4,7 @@ import HandleBars from '../../lib/handleBars.js';
|
|
|
4
4
|
import { request } from '../../lib/request.js';
|
|
5
5
|
import { encode } from 'gpt-3-encoder';
|
|
6
6
|
import { getFirstNToken } from '../chunker.js';
|
|
7
|
+
import logger from '../../lib/logger.js';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
9
10
|
const DEFAULT_MAX_RETURN_TOKENS = 256;
|
|
@@ -224,8 +225,9 @@ class ModelPlugin {
|
|
|
224
225
|
this.lastRequestStartTime = new Date();
|
|
225
226
|
const logMessage = `>>> [${this.requestId}: ${this.pathwayName}.${this.requestCount}] request`;
|
|
226
227
|
const header = '>'.repeat(logMessage.length);
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
logger.info(`${header}`);
|
|
229
|
+
logger.info(`${logMessage}`);
|
|
230
|
+
logger.info(`>>> Making API request to ${url}`);
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
logAIRequestFinished() {
|
|
@@ -233,7 +235,8 @@ class ModelPlugin {
|
|
|
233
235
|
const timeElapsed = (currentTime - this.lastRequestStartTime) / 1000;
|
|
234
236
|
const logMessage = `<<< [${this.requestId}: ${this.pathwayName}] response - complete in ${timeElapsed}s - data:`;
|
|
235
237
|
const header = '<'.repeat(logMessage.length);
|
|
236
|
-
|
|
238
|
+
logger.info(`${header}`);
|
|
239
|
+
logger.info(`${logMessage}`);
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
logRequestData(data, responseData, prompt) {
|
|
@@ -241,10 +244,15 @@ class ModelPlugin {
|
|
|
241
244
|
const modelInput = data.prompt || (data.messages && data.messages[0].content) || (data.length > 0 && data[0].Text) || null;
|
|
242
245
|
|
|
243
246
|
if (modelInput) {
|
|
244
|
-
|
|
247
|
+
const inputTokens = encode(modelInput).length;
|
|
248
|
+
logger.info(`[request sent containing ${inputTokens} tokens]`);
|
|
249
|
+
logger.debug(`${modelInput}`);
|
|
245
250
|
}
|
|
246
251
|
|
|
247
|
-
|
|
252
|
+
const responseText = JSON.stringify(this.parseResponse(responseData));
|
|
253
|
+
const responseTokens = encode(responseText).length;
|
|
254
|
+
logger.info(`[response received containing ${responseTokens} tokens]`);
|
|
255
|
+
logger.debug(`${responseText}`);
|
|
248
256
|
|
|
249
257
|
prompt && prompt.debugInfo && (prompt.debugInfo += `\n${JSON.stringify(data)}`);
|
|
250
258
|
}
|
|
@@ -264,7 +272,8 @@ class ModelPlugin {
|
|
|
264
272
|
return this.parseResponse(responseData);
|
|
265
273
|
} catch (error) {
|
|
266
274
|
// Log the error and continue
|
|
267
|
-
|
|
275
|
+
logger.error(error.message || error);
|
|
276
|
+
throw error;
|
|
268
277
|
}
|
|
269
278
|
}
|
|
270
279
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// OpenAIChatPlugin.js
|
|
2
2
|
import ModelPlugin from './modelPlugin.js';
|
|
3
3
|
import { encode } from 'gpt-3-encoder';
|
|
4
|
+
import logger from '../../lib/logger.js';
|
|
4
5
|
|
|
5
6
|
class OpenAIChatPlugin extends ModelPlugin {
|
|
6
7
|
constructor(config, pathway, modelName, model) {
|
|
@@ -110,6 +111,8 @@ class OpenAIChatPlugin extends ModelPlugin {
|
|
|
110
111
|
|
|
111
112
|
const { stream, messages } = data;
|
|
112
113
|
if (messages && messages.length > 1) {
|
|
114
|
+
logger.info(`[chat request sent containing ${messages.length} messages]`);
|
|
115
|
+
let totalTokens = 0;
|
|
113
116
|
messages.forEach((message, index) => {
|
|
114
117
|
//message.content string or array
|
|
115
118
|
const content = Array.isArray(message.content) ? message.content.map(item => JSON.stringify(item)).join(', ') : message.content;
|
|
@@ -117,16 +120,25 @@ class OpenAIChatPlugin extends ModelPlugin {
|
|
|
117
120
|
const tokenCount = encode(content).length;
|
|
118
121
|
const preview = words.length < 41 ? content : words.slice(0, 20).join(" ") + " ... " + words.slice(-20).join(" ");
|
|
119
122
|
|
|
120
|
-
|
|
123
|
+
logger.debug(`Message ${index + 1}: Role: ${message.role}, Tokens: ${tokenCount}, Content: "${preview}"`);
|
|
124
|
+
totalTokens += tokenCount;
|
|
121
125
|
});
|
|
126
|
+
logger.info(`[chat request contained ${totalTokens} tokens]`);
|
|
122
127
|
} else {
|
|
123
|
-
|
|
128
|
+
const message = messages[0];
|
|
129
|
+
const content = Array.isArray(message.content) ? message.content.map(item => JSON.stringify(item)).join(', ') : message.content;
|
|
130
|
+
const tokenCount = encode(content).length;
|
|
131
|
+
logger.info(`[request sent containing ${tokenCount} tokens]`);
|
|
132
|
+
logger.debug(`${content}`);
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
if (stream) {
|
|
127
|
-
|
|
136
|
+
logger.info(`[response received as an SSE stream]`);
|
|
128
137
|
} else {
|
|
129
|
-
|
|
138
|
+
const responseText = this.parseResponse(responseData);
|
|
139
|
+
const responseTokens = encode(responseText).length;
|
|
140
|
+
logger.info(`[response received containing ${responseTokens} tokens]`);
|
|
141
|
+
logger.debug(`${responseText}`);
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
prompt && prompt.debugInfo && (prompt.debugInfo += `\n${JSON.stringify(data)}`);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { request } from 'https';
|
|
4
4
|
import ModelPlugin from './modelPlugin.js';
|
|
5
5
|
import { encode } from 'gpt-3-encoder';
|
|
6
|
+
import logger from '../../lib/logger.js';
|
|
6
7
|
|
|
7
8
|
// Helper function to truncate the prompt if it is too long
|
|
8
9
|
const truncatePromptIfNecessary = (text, textTokenCount, modelMaxTokenCount, targetTextTokenCount, pathwayResolver) => {
|
|
@@ -112,12 +113,17 @@ class OpenAICompletionPlugin extends ModelPlugin {
|
|
|
112
113
|
const stream = data.stream;
|
|
113
114
|
const modelInput = data.prompt;
|
|
114
115
|
|
|
115
|
-
|
|
116
|
+
const modelInputTokens = encode(modelInput).length;
|
|
117
|
+
logger.info(`[request sent containing ${modelInputTokens} tokens]`);
|
|
118
|
+
logger.debug(`${modelInput}`);
|
|
116
119
|
|
|
117
120
|
if (stream) {
|
|
118
|
-
|
|
121
|
+
logger.info(`[response received as an SSE stream]`);
|
|
119
122
|
} else {
|
|
120
|
-
|
|
123
|
+
const responseText = this.parseResponse(responseData);
|
|
124
|
+
const responseTokens = encode(responseText).length;
|
|
125
|
+
logger.info(`[response received containing ${responseTokens} tokens]`);
|
|
126
|
+
logger.debug(`${responseText}`);
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
prompt && prompt.debugInfo && (prompt.debugInfo += `\n${JSON.stringify(data)}`);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import RequestDurationEstimator from '../../lib/requestDurationEstimator.js';
|
|
2
|
-
import pubsub from '../pubsub.js';
|
|
3
2
|
import ModelPlugin from './modelPlugin.js';
|
|
4
3
|
import { request } from '../../lib/request.js';
|
|
4
|
+
import { publishRequestProgress } from '../../lib/redisSubscription.js';
|
|
5
|
+
import logger from '../../lib/logger.js';
|
|
5
6
|
|
|
6
7
|
const requestDurationEstimator = new RequestDurationEstimator(10);
|
|
7
8
|
|
|
@@ -30,7 +31,7 @@ class OpenAIDallE3Plugin extends ModelPlugin {
|
|
|
30
31
|
return this.parseResponse(responseData);
|
|
31
32
|
} catch (error) {
|
|
32
33
|
// Log the error and continue
|
|
33
|
-
|
|
34
|
+
logger.error(error.message || error);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -83,7 +84,7 @@ class OpenAIDallE3Plugin extends ModelPlugin {
|
|
|
83
84
|
|
|
84
85
|
state.status = status;
|
|
85
86
|
requestDurationEstimator.endRequest();
|
|
86
|
-
|
|
87
|
+
publishRequestProgress(requestProgress);
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
// publish an update every 2 seconds, using the request duration estimator to calculate
|
|
@@ -92,14 +93,12 @@ class OpenAIDallE3Plugin extends ModelPlugin {
|
|
|
92
93
|
let progress =
|
|
93
94
|
requestDurationEstimator.calculatePercentComplete();
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
requestProgress: {
|
|
96
|
+
await publishRequestProgress({
|
|
97
97
|
requestId,
|
|
98
98
|
status: "pending",
|
|
99
99
|
progress,
|
|
100
100
|
data,
|
|
101
|
-
}
|
|
102
|
-
});
|
|
101
|
+
});
|
|
103
102
|
|
|
104
103
|
if (state.status !== "pending") {
|
|
105
104
|
break;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// OpenAIImagePlugin.js
|
|
2
|
-
import FormData from 'form-data';
|
|
3
|
-
import { config } from '../../config.js';
|
|
4
2
|
import ModelPlugin from './modelPlugin.js';
|
|
5
|
-
import pubsub from '../pubsub.js';
|
|
6
3
|
import axios from 'axios';
|
|
7
4
|
import RequestDurationEstimator from '../../lib/requestDurationEstimator.js';
|
|
5
|
+
import { publishRequestProgress } from '../../lib/redisSubscription.js';
|
|
6
|
+
import logger from '../../lib/logger.js';
|
|
8
7
|
|
|
9
8
|
const requestDurationEstimator = new RequestDurationEstimator(10);
|
|
10
9
|
|
|
@@ -26,7 +25,7 @@ class OpenAIImagePlugin extends ModelPlugin {
|
|
|
26
25
|
id = (await this.executeRequest(url, data, {}, { ...this.model.headers }, {}, requestId, pathway))?.id;
|
|
27
26
|
} catch (error) {
|
|
28
27
|
const errMsg = `Error generating image: ${error?.message || JSON.stringify(error)}`;
|
|
29
|
-
|
|
28
|
+
logger.error(errMsg);
|
|
30
29
|
return errMsg;
|
|
31
30
|
}
|
|
32
31
|
|
|
@@ -59,13 +58,11 @@ class OpenAIImagePlugin extends ModelPlugin {
|
|
|
59
58
|
data = JSON.stringify(response);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
requestProgress: {
|
|
61
|
+
await publishRequestProgress({
|
|
64
62
|
requestId,
|
|
65
63
|
status,
|
|
66
64
|
progress,
|
|
67
65
|
data,
|
|
68
|
-
}
|
|
69
66
|
});
|
|
70
67
|
|
|
71
68
|
if (status === "succeeded") {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import ModelPlugin from './modelPlugin.js';
|
|
3
3
|
import FormData from 'form-data';
|
|
4
4
|
import fs from 'fs';
|
|
5
|
-
import pubsub from '../pubsub.js';
|
|
6
5
|
import { axios } from '../../lib/request.js';
|
|
7
6
|
import stream from 'stream';
|
|
8
7
|
import os from 'os';
|
|
@@ -14,8 +13,10 @@ import http from 'http';
|
|
|
14
13
|
import https from 'https';
|
|
15
14
|
import { promisify } from 'util';
|
|
16
15
|
import subsrt from 'subsrt';
|
|
17
|
-
|
|
16
|
+
import { publishRequestProgress } from '../../lib/redisSubscription.js';
|
|
17
|
+
import logger from '../../lib/logger.js';
|
|
18
18
|
|
|
19
|
+
const pipeline = promisify(stream.pipeline);
|
|
19
20
|
|
|
20
21
|
const API_URL = config.get('whisperMediaApiUrl');
|
|
21
22
|
const WHISPER_TS_API_URL = config.get('whisperTSApiUrl');
|
|
@@ -47,7 +48,7 @@ function alignSubtitles(subtitles, format) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
} catch (error) {
|
|
50
|
-
|
|
51
|
+
logger.error(`An error occurred in content text parsing: ${error}`);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
return subsrt.build(result, { format: format === 'vtt' ? 'vtt' : 'srt' });
|
|
@@ -80,7 +81,7 @@ const downloadFile = async (fileUrl) => {
|
|
|
80
81
|
});
|
|
81
82
|
|
|
82
83
|
await pipeline(response, fs.createWriteStream(localFilePath));
|
|
83
|
-
|
|
84
|
+
logger.info(`Downloaded file to ${localFilePath}`);
|
|
84
85
|
resolve(localFilePath);
|
|
85
86
|
} catch (error) {
|
|
86
87
|
fs.unlink(localFilePath, () => {
|
|
@@ -103,11 +104,11 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
103
104
|
const res = await axios.get(API_URL, { params: { uri: file, requestId } });
|
|
104
105
|
return res.data;
|
|
105
106
|
} else {
|
|
106
|
-
|
|
107
|
+
logger.info(`No API_URL set, returning file as chunk`);
|
|
107
108
|
return [file];
|
|
108
109
|
}
|
|
109
110
|
} catch (err) {
|
|
110
|
-
|
|
111
|
+
logger.error(`Error getting media chunks list from api:`, err);
|
|
111
112
|
throw err;
|
|
112
113
|
}
|
|
113
114
|
}
|
|
@@ -117,11 +118,11 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
117
118
|
if (API_URL) {
|
|
118
119
|
//call helper api to mark processing as completed
|
|
119
120
|
const res = await axios.delete(API_URL, { params: { requestId } });
|
|
120
|
-
|
|
121
|
+
logger.info(`Marked request ${requestId} as completed: ${res.data}`);
|
|
121
122
|
return res.data;
|
|
122
123
|
}
|
|
123
124
|
} catch (err) {
|
|
124
|
-
|
|
125
|
+
logger.error(`Error marking request ${requestId} as completed: ${err}`);
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -149,7 +150,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
149
150
|
const res = await this.executeRequest(WHISPER_TS_API_URL, tsparams, {}, {}, {}, requestId, pathway);
|
|
150
151
|
return res;
|
|
151
152
|
} catch (err) {
|
|
152
|
-
|
|
153
|
+
logger.error(`Error getting word timestamped data from api: ${err}`);
|
|
153
154
|
throw err;
|
|
154
155
|
}
|
|
155
156
|
}
|
|
@@ -169,7 +170,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
169
170
|
|
|
170
171
|
return this.executeRequest(url, formData, params, { ...this.model.headers, ...formData.getHeaders() }, {}, requestId, pathway);
|
|
171
172
|
} catch (err) {
|
|
172
|
-
|
|
173
|
+
logger.error(err);
|
|
173
174
|
throw err;
|
|
174
175
|
}
|
|
175
176
|
}
|
|
@@ -183,13 +184,12 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
183
184
|
const sendProgress = () => {
|
|
184
185
|
completedCount++;
|
|
185
186
|
if (completedCount >= totalCount) return;
|
|
186
|
-
|
|
187
|
-
requestProgress: {
|
|
187
|
+
publishRequestProgress({
|
|
188
188
|
requestId,
|
|
189
189
|
progress: completedCount / totalCount,
|
|
190
190
|
data: null,
|
|
191
|
-
}
|
|
192
191
|
});
|
|
192
|
+
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
let chunks = []; // array of local file paths
|
|
@@ -225,7 +225,7 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
225
225
|
|
|
226
226
|
} catch (error) {
|
|
227
227
|
const errMsg = `Transcribe error: ${error?.message || JSON.stringify(error)}`;
|
|
228
|
-
|
|
228
|
+
logger.error(errMsg);
|
|
229
229
|
return errMsg;
|
|
230
230
|
}
|
|
231
231
|
finally {
|
|
@@ -242,11 +242,11 @@ class OpenAIWhisperPlugin extends ModelPlugin {
|
|
|
242
242
|
if (match && match[1]) {
|
|
243
243
|
const extractedValue = match[1];
|
|
244
244
|
await this.markCompletedForCleanUp(extractedValue);
|
|
245
|
-
|
|
245
|
+
logger.info(`Cleaned temp whisper file ${file} with request id ${extractedValue}`);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
} catch (error) {
|
|
249
|
-
|
|
249
|
+
logger.error(`An error occurred while deleting: ${error}`);
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import ModelPlugin from './modelPlugin.js';
|
|
3
3
|
import { encode } from 'gpt-3-encoder';
|
|
4
4
|
import HandleBars from '../../lib/handleBars.js';
|
|
5
|
+
import logger from '../../lib/logger.js';
|
|
5
6
|
|
|
6
7
|
class PalmChatPlugin extends ModelPlugin {
|
|
7
8
|
constructor(config, pathway, modelName, model) {
|
|
@@ -189,33 +190,40 @@ class PalmChatPlugin extends ModelPlugin {
|
|
|
189
190
|
const { context, examples } = instances && instances [0] || {};
|
|
190
191
|
|
|
191
192
|
if (context) {
|
|
192
|
-
|
|
193
|
+
const contextLength = encode(context).length;
|
|
194
|
+
logger.info(`[chat request contains context information of length ${contextLength} tokens]`)
|
|
195
|
+
logger.debug(`Context: ${context}`);
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
if (examples && examples.length) {
|
|
199
|
+
logger.info(`[chat request contains ${examples.length} examples]`);
|
|
196
200
|
examples.forEach((example, index) => {
|
|
197
|
-
|
|
201
|
+
logger.debug(`Example ${index + 1}: Input: "${example.input.content}", Output: "${example.output.content}"`);
|
|
198
202
|
});
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
if (messages && messages.length > 1) {
|
|
206
|
+
logger.info(`[chat request contains ${messages.length} messages]`);
|
|
202
207
|
messages.forEach((message, index) => {
|
|
203
208
|
const words = message.content.split(" ");
|
|
204
209
|
const tokenCount = encode(message.content).length;
|
|
205
210
|
const preview = words.length < 41 ? message.content : words.slice(0, 20).join(" ") + " ... " + words.slice(-20).join(" ");
|
|
206
211
|
|
|
207
|
-
|
|
212
|
+
logger.debug(`Message ${index + 1}: Author: ${message.author}, Tokens: ${tokenCount}, Content: "${preview}"`);
|
|
208
213
|
});
|
|
209
214
|
} else if (messages && messages.length === 1) {
|
|
210
|
-
|
|
215
|
+
logger.debug(`${messages[0].content}`);
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
const safetyAttributes = this.getSafetyAttributes(responseData);
|
|
214
219
|
|
|
215
|
-
|
|
220
|
+
const responseText = this.parseResponse(responseData);
|
|
221
|
+
const responseTokens = encode(responseText).length;
|
|
222
|
+
logger.info(`[response received containing ${responseTokens} tokens]`);
|
|
223
|
+
logger.debug(`${responseText}`);
|
|
216
224
|
|
|
217
225
|
if (safetyAttributes) {
|
|
218
|
-
|
|
226
|
+
logger.warn(`[response contains safety attributes: ${JSON.stringify(safetyAttributes, null, 2)}]`);
|
|
219
227
|
}
|
|
220
228
|
|
|
221
229
|
if (prompt && prompt.debugInfo) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// palmCompletionPlugin.js
|
|
2
2
|
|
|
3
3
|
import ModelPlugin from './modelPlugin.js';
|
|
4
|
+
import { encode } from 'gpt-3-encoder';
|
|
5
|
+
import logger from '../../lib/logger.js';
|
|
4
6
|
|
|
5
7
|
// PalmCompletionPlugin class for handling requests and responses to the PaLM API Text Completion API
|
|
6
8
|
class PalmCompletionPlugin extends ModelPlugin {
|
|
@@ -114,13 +116,18 @@ class PalmCompletionPlugin extends ModelPlugin {
|
|
|
114
116
|
const modelInput = instances && instances[0] && instances[0].prompt;
|
|
115
117
|
|
|
116
118
|
if (modelInput) {
|
|
117
|
-
|
|
119
|
+
const inputTokens = encode(modelInput).length;
|
|
120
|
+
logger.info(`[request sent containing ${inputTokens} tokens]`);
|
|
121
|
+
logger.debug(`${modelInput}`);
|
|
118
122
|
}
|
|
119
123
|
|
|
120
|
-
|
|
124
|
+
const responseText = this.parseResponse(responseData);
|
|
125
|
+
const responseTokens = encode(responseText).length;
|
|
126
|
+
logger.info(`[response received containing ${responseTokens} tokens]`);
|
|
127
|
+
logger.debug(`${responseText}`);
|
|
121
128
|
|
|
122
129
|
if (safetyAttributes) {
|
|
123
|
-
|
|
130
|
+
logger.warn(`[response contains safety attributes: ${JSON.stringify(safetyAttributes, null, 2)}]`);
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
if (prompt && prompt.debugInfo) {
|
package/server/pubsub.js
CHANGED
package/server/rest.js
CHANGED
|
@@ -5,6 +5,7 @@ import { json } from 'express';
|
|
|
5
5
|
import pubsub from './pubsub.js';
|
|
6
6
|
import { requestState } from './requestState.js';
|
|
7
7
|
import { v4 as uuidv4 } from 'uuid';
|
|
8
|
+
import logger from '../lib/logger.js';
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
const processRestRequest = async (server, req, pathway, name, parameterMap = {}) => {
|
|
@@ -85,7 +86,7 @@ const processIncomingStream = (requestId, res, jsonResponse) => {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
const sendStreamData = (data) => {
|
|
88
|
-
//
|
|
89
|
+
//logger.info(`REST SEND: data: ${JSON.stringify(data)}`);
|
|
89
90
|
const dataString = (data==='[DONE]') ? data : JSON.stringify(data);
|
|
90
91
|
|
|
91
92
|
if (!res.writableEnded) {
|
|
@@ -116,13 +117,13 @@ const processIncomingStream = (requestId, res, jsonResponse) => {
|
|
|
116
117
|
try {
|
|
117
118
|
pubsub.unsubscribe(await subscription);
|
|
118
119
|
} catch (error) {
|
|
119
|
-
|
|
120
|
+
logger.error(`Error unsubscribing from pubsub: ${error}`);
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
if (data.requestProgress.requestId === requestId) {
|
|
125
|
-
//
|
|
126
|
+
//logger.info(`REQUEST_PROGRESS received progress: ${data.requestProgress.progress}, data: ${data.requestProgress.data}`);
|
|
126
127
|
|
|
127
128
|
const progress = data.requestProgress.progress;
|
|
128
129
|
const progressData = data.requestProgress.data;
|
|
@@ -130,7 +131,7 @@ const processIncomingStream = (requestId, res, jsonResponse) => {
|
|
|
130
131
|
try {
|
|
131
132
|
const messageJson = JSON.parse(progressData);
|
|
132
133
|
if (messageJson.error) {
|
|
133
|
-
|
|
134
|
+
logger.error(`Stream error REST: ${messageJson?.error?.message || 'unknown error'}`);
|
|
134
135
|
safeUnsubscribe();
|
|
135
136
|
finishStream(res, jsonResponse);
|
|
136
137
|
return;
|
|
@@ -146,7 +147,7 @@ const processIncomingStream = (requestId, res, jsonResponse) => {
|
|
|
146
147
|
fillJsonResponse(jsonResponse, messageJson, null);
|
|
147
148
|
}
|
|
148
149
|
} catch (error) {
|
|
149
|
-
//
|
|
150
|
+
//logger.info(`progressData not JSON: ${progressData}`);
|
|
150
151
|
fillJsonResponse(jsonResponse, progressData, "stop");
|
|
151
152
|
}
|
|
152
153
|
if (progress === 1 && progressData.trim() === "[DONE]") {
|
|
@@ -165,7 +166,7 @@ const processIncomingStream = (requestId, res, jsonResponse) => {
|
|
|
165
166
|
});
|
|
166
167
|
|
|
167
168
|
// Fire the resolver for the async requestProgress
|
|
168
|
-
|
|
169
|
+
logger.info(`Rest Endpoint starting async requestProgress, requestId: ${requestId}`);
|
|
169
170
|
const { resolver, args } = requestState[requestId];
|
|
170
171
|
resolver(args);
|
|
171
172
|
|
package/server/subscriptions.js
CHANGED
|
@@ -1,25 +1,13 @@
|
|
|
1
|
-
// TODO: Replace PubSub class with PubSub engine to support
|
|
2
|
-
// multi-server instance
|
|
3
|
-
// See https://www.apollographql.com/docs/apollo-server/v3/data/subscriptions/#resolving-a-subscription
|
|
4
|
-
|
|
5
1
|
import pubsub from './pubsub.js';
|
|
6
|
-
|
|
2
|
+
import logger from '../lib/logger.js';
|
|
7
3
|
import { withFilter } from 'graphql-subscriptions';
|
|
8
|
-
import {
|
|
4
|
+
import { publishRequestProgressSubscription } from '../lib/redisSubscription.js';
|
|
9
5
|
|
|
10
6
|
const subscriptions = {
|
|
11
7
|
requestProgress: {
|
|
12
8
|
subscribe: withFilter(
|
|
13
9
|
(_, args, __, _info) => {
|
|
14
|
-
|
|
15
|
-
for (const requestId of requestIds) {
|
|
16
|
-
if (requestState[requestId] && !requestState[requestId].started) {
|
|
17
|
-
requestState[requestId].started = true;
|
|
18
|
-
console.log(`Subscription starting async requestProgress, requestId: ${requestId}`);
|
|
19
|
-
const { resolver, args } = requestState[requestId];
|
|
20
|
-
resolver(args);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
10
|
+
publishRequestProgressSubscription(args.requestIds);
|
|
23
11
|
return pubsub.asyncIterator(['REQUEST_PROGRESS'])
|
|
24
12
|
},
|
|
25
13
|
(payload, variables) => {
|
|
@@ -217,41 +217,3 @@ test('parseResponse', (t) => {
|
|
|
217
217
|
|
|
218
218
|
t.is(palmChatPlugin.parseResponse(responseData), expectedResult);
|
|
219
219
|
});
|
|
220
|
-
|
|
221
|
-
test('logRequestData', (t) => {
|
|
222
|
-
const { palmChatPlugin } = t.context;
|
|
223
|
-
const data = {
|
|
224
|
-
instances: [
|
|
225
|
-
{
|
|
226
|
-
messages: [
|
|
227
|
-
{ author: 'user', content: 'Hello' },
|
|
228
|
-
{ author: 'assistant', content: 'How can I help you?' },
|
|
229
|
-
],
|
|
230
|
-
},
|
|
231
|
-
],
|
|
232
|
-
};
|
|
233
|
-
const responseData = {
|
|
234
|
-
predictions: [
|
|
235
|
-
{
|
|
236
|
-
candidates: [
|
|
237
|
-
{
|
|
238
|
-
content: 'Hello, how can I help you today?',
|
|
239
|
-
},
|
|
240
|
-
],
|
|
241
|
-
},
|
|
242
|
-
],
|
|
243
|
-
};
|
|
244
|
-
const prompt = { debugInfo: '' };
|
|
245
|
-
|
|
246
|
-
const consoleLog = console.log;
|
|
247
|
-
let logOutput = '';
|
|
248
|
-
console.log = (msg) => (logOutput += msg + '\n');
|
|
249
|
-
|
|
250
|
-
palmChatPlugin.logRequestData(data, responseData, prompt);
|
|
251
|
-
|
|
252
|
-
console.log = consoleLog;
|
|
253
|
-
|
|
254
|
-
t.true(logOutput.includes('Message 1:'));
|
|
255
|
-
t.true(logOutput.includes('Message 2:'));
|
|
256
|
-
t.true(logOutput.includes('> Hello, how can I help you today?'));
|
|
257
|
-
});
|
|
@@ -56,33 +56,3 @@ test('getSafetyAttributes', (t) => {
|
|
|
56
56
|
|
|
57
57
|
t.deepEqual(palmCompletionPlugin.getSafetyAttributes(responseData), expectedResult);
|
|
58
58
|
});
|
|
59
|
-
|
|
60
|
-
test('logRequestData', (t) => {
|
|
61
|
-
const { palmCompletionPlugin } = t.context;
|
|
62
|
-
const data = {
|
|
63
|
-
instances: [
|
|
64
|
-
{
|
|
65
|
-
prompt: 'Hello, how can I help you?',
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
};
|
|
69
|
-
const responseData = {
|
|
70
|
-
predictions: [
|
|
71
|
-
{
|
|
72
|
-
content: 'Hello, how can I help you today?',
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
};
|
|
76
|
-
const prompt = { debugInfo: '' };
|
|
77
|
-
|
|
78
|
-
const consoleLog = console.log;
|
|
79
|
-
let logOutput = '';
|
|
80
|
-
console.log = (msg) => (logOutput += msg + '\n');
|
|
81
|
-
|
|
82
|
-
palmCompletionPlugin.logRequestData(data, responseData, prompt);
|
|
83
|
-
|
|
84
|
-
console.log = consoleLog;
|
|
85
|
-
|
|
86
|
-
t.true(logOutput.includes('Hello, how can I help you?'));
|
|
87
|
-
t.true(logOutput.includes('> Hello, how can I help you today?'));
|
|
88
|
-
});
|