@aj-archipelago/cortex 1.3.21 → 1.3.23
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 +64 -0
- package/config.js +26 -1
- package/helper-apps/cortex-realtime-voice-server/src/cortex/memory.ts +2 -2
- package/helper-apps/cortex-realtime-voice-server/src/realtime/client.ts +9 -4
- package/helper-apps/cortex-realtime-voice-server/src/realtime/realtimeTypes.ts +1 -0
- package/lib/util.js +5 -25
- package/package.json +5 -2
- package/pathways/system/entity/memory/shared/sys_memory_helpers.js +228 -0
- package/pathways/system/entity/memory/sys_memory_format.js +30 -0
- package/pathways/system/entity/memory/sys_memory_manager.js +85 -27
- package/pathways/system/entity/memory/sys_memory_process.js +154 -0
- package/pathways/system/entity/memory/sys_memory_required.js +4 -2
- package/pathways/system/entity/memory/sys_memory_topic.js +22 -0
- package/pathways/system/entity/memory/sys_memory_update.js +50 -150
- package/pathways/system/entity/memory/sys_read_memory.js +67 -69
- package/pathways/system/entity/memory/sys_save_memory.js +1 -1
- package/pathways/system/entity/memory/sys_search_memory.js +1 -1
- package/pathways/system/entity/sys_entity_start.js +9 -6
- package/pathways/system/entity/sys_generator_image.js +5 -41
- package/pathways/system/entity/sys_generator_memory.js +3 -1
- package/pathways/system/entity/sys_generator_reasoning.js +1 -1
- package/pathways/system/entity/sys_router_tool.js +3 -4
- package/pathways/system/rest_streaming/sys_claude_35_sonnet.js +1 -1
- package/pathways/system/rest_streaming/sys_claude_3_haiku.js +1 -1
- package/pathways/system/rest_streaming/sys_google_gemini_chat.js +1 -1
- package/pathways/system/rest_streaming/sys_ollama_chat.js +21 -0
- package/pathways/system/rest_streaming/sys_ollama_completion.js +14 -0
- package/pathways/system/rest_streaming/sys_openai_chat_o1.js +1 -1
- package/pathways/system/rest_streaming/sys_openai_chat_o3_mini.js +1 -1
- package/pathways/transcribe_gemini.js +525 -0
- package/server/modelExecutor.js +8 -0
- package/server/pathwayResolver.js +13 -8
- package/server/plugins/claude3VertexPlugin.js +150 -18
- package/server/plugins/gemini15ChatPlugin.js +90 -1
- package/server/plugins/gemini15VisionPlugin.js +16 -3
- package/server/plugins/modelPlugin.js +12 -9
- package/server/plugins/ollamaChatPlugin.js +158 -0
- package/server/plugins/ollamaCompletionPlugin.js +147 -0
- package/server/rest.js +70 -8
- package/tests/claude3VertexToolConversion.test.js +411 -0
- package/tests/memoryfunction.test.js +560 -46
- package/tests/multimodal_conversion.test.js +169 -0
- package/tests/openai_api.test.js +332 -0
- package/tests/transcribe_gemini.test.js +217 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import ModelPlugin from './modelPlugin.js';
|
|
2
|
+
import logger from '../../lib/logger.js';
|
|
3
|
+
import { Transform } from 'stream';
|
|
4
|
+
|
|
5
|
+
class OllamaCompletionPlugin extends ModelPlugin {
|
|
6
|
+
|
|
7
|
+
getRequestParameters(text, parameters, prompt) {
|
|
8
|
+
return {
|
|
9
|
+
data: {
|
|
10
|
+
model: parameters.ollamaModel,
|
|
11
|
+
prompt: text,
|
|
12
|
+
stream: parameters.stream
|
|
13
|
+
},
|
|
14
|
+
params: {}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
logRequestData(data, responseData, prompt) {
|
|
19
|
+
const { stream, prompt: promptText, model } = data;
|
|
20
|
+
|
|
21
|
+
if (promptText) {
|
|
22
|
+
logger.info(`[ollama completion request sent to model ${model}]`);
|
|
23
|
+
const { length, units } = this.getLength(promptText);
|
|
24
|
+
const preview = this.shortenContent(promptText);
|
|
25
|
+
logger.verbose(`prompt ${units}: ${length}, content: "${preview}"`);
|
|
26
|
+
logger.info(`[completion request contained ${length} ${units}]`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (stream) {
|
|
30
|
+
logger.info(`[response received as an SSE stream]`);
|
|
31
|
+
} else if (responseData) {
|
|
32
|
+
const responseText = this.parseResponse(responseData);
|
|
33
|
+
const { length, units } = this.getLength(responseText);
|
|
34
|
+
logger.info(`[response received containing ${length} ${units}]`);
|
|
35
|
+
logger.verbose(`${this.shortenContent(responseText)}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
prompt &&
|
|
39
|
+
prompt.debugInfo &&
|
|
40
|
+
(prompt.debugInfo += `\n${JSON.stringify(data)}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
parseResponse(data) {
|
|
44
|
+
// If data is not a string (e.g. streaming), return as is
|
|
45
|
+
if (typeof data !== 'string') {
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Split into lines and filter empty ones
|
|
50
|
+
const lines = data.split('\n').filter(line => line.trim());
|
|
51
|
+
|
|
52
|
+
let fullResponse = '';
|
|
53
|
+
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
try {
|
|
56
|
+
const jsonObj = JSON.parse(line);
|
|
57
|
+
|
|
58
|
+
if (jsonObj.response) {
|
|
59
|
+
// Unescape special sequences
|
|
60
|
+
const content = jsonObj.response
|
|
61
|
+
.replace(/\\n/g, '\n')
|
|
62
|
+
.replace(/\\"/g, '"')
|
|
63
|
+
.replace(/\\\\/g, '\\')
|
|
64
|
+
.replace(/\\u003c/g, '<')
|
|
65
|
+
.replace(/\\u003e/g, '>');
|
|
66
|
+
|
|
67
|
+
fullResponse += content;
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// If we can't parse the line as JSON, just skip it
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return fullResponse;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
processStreamEvent(event, requestProgress) {
|
|
79
|
+
try {
|
|
80
|
+
const data = JSON.parse(event.data);
|
|
81
|
+
|
|
82
|
+
// Handle the streaming response
|
|
83
|
+
if (data.response) {
|
|
84
|
+
// Unescape special sequences in the content
|
|
85
|
+
const content = data.response
|
|
86
|
+
.replace(/\\n/g, '\n')
|
|
87
|
+
.replace(/\\"/g, '"')
|
|
88
|
+
.replace(/\\\\/g, '\\')
|
|
89
|
+
.replace(/\\u003c/g, '<')
|
|
90
|
+
.replace(/\\u003e/g, '>');
|
|
91
|
+
|
|
92
|
+
requestProgress.data = JSON.stringify(content);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check if this is the final message
|
|
96
|
+
if (data.done) {
|
|
97
|
+
requestProgress.data = '[DONE]';
|
|
98
|
+
requestProgress.progress = 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return requestProgress;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
// If we can't parse the event data, return the progress as is
|
|
104
|
+
return requestProgress;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async execute(text, parameters, prompt, cortexRequest) {
|
|
109
|
+
const requestParameters = this.getRequestParameters(text, parameters, prompt);
|
|
110
|
+
cortexRequest.data = { ...(cortexRequest.data || {}), ...requestParameters.data };
|
|
111
|
+
cortexRequest.params = { ...(cortexRequest.params || {}), ...requestParameters.params };
|
|
112
|
+
|
|
113
|
+
// For Ollama streaming, transform NDJSON to SSE format
|
|
114
|
+
if (parameters.stream) {
|
|
115
|
+
const response = await this.executeRequest(cortexRequest);
|
|
116
|
+
|
|
117
|
+
// Create a transform stream that converts NDJSON to SSE format
|
|
118
|
+
const transformer = new Transform({
|
|
119
|
+
decodeStrings: false, // Keep as string
|
|
120
|
+
transform(chunk, encoding, callback) {
|
|
121
|
+
try {
|
|
122
|
+
const lines = chunk.toString().split('\n');
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
if (line.trim()) {
|
|
125
|
+
// Format as SSE data
|
|
126
|
+
this.push(`data: ${line}\n\n`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
callback();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
callback(err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Pipe the response through our transformer
|
|
137
|
+
response.pipe(transformer);
|
|
138
|
+
|
|
139
|
+
// Return the transformed stream
|
|
140
|
+
return transformer;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return this.executeRequest(cortexRequest);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default OllamaCompletionPlugin;
|
package/server/rest.js
CHANGED
|
@@ -6,6 +6,22 @@ import { requestState } from './requestState.js';
|
|
|
6
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
7
7
|
import logger from '../lib/logger.js';
|
|
8
8
|
import { getSingleTokenChunks } from './chunker.js';
|
|
9
|
+
import axios from 'axios';
|
|
10
|
+
|
|
11
|
+
const getOllamaModels = async (ollamaUrl) => {
|
|
12
|
+
try {
|
|
13
|
+
const response = await axios.get(`${ollamaUrl}/api/tags`);
|
|
14
|
+
return response.data.models.map(model => ({
|
|
15
|
+
id: `ollama-${model.name}`,
|
|
16
|
+
object: 'model',
|
|
17
|
+
owned_by: 'ollama',
|
|
18
|
+
permission: ''
|
|
19
|
+
}));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
logger.error(`Error fetching Ollama models: ${error.message}`);
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
};
|
|
9
25
|
|
|
10
26
|
const chunkTextIntoTokens = (() => {
|
|
11
27
|
let partialToken = '';
|
|
@@ -28,6 +44,13 @@ const processRestRequest = async (server, req, pathway, name, parameterMap = {})
|
|
|
28
44
|
return Boolean(value);
|
|
29
45
|
} else if (type === 'Int') {
|
|
30
46
|
return parseInt(value, 10);
|
|
47
|
+
} else if (type === '[MultiMessage]' && Array.isArray(value)) {
|
|
48
|
+
return value.map(msg => ({
|
|
49
|
+
...msg,
|
|
50
|
+
content: Array.isArray(msg.content) ?
|
|
51
|
+
JSON.stringify(msg.content) :
|
|
52
|
+
msg.content
|
|
53
|
+
}));
|
|
31
54
|
} else {
|
|
32
55
|
return value;
|
|
33
56
|
}
|
|
@@ -58,8 +81,16 @@ const processRestRequest = async (server, req, pathway, name, parameterMap = {})
|
|
|
58
81
|
`;
|
|
59
82
|
|
|
60
83
|
const result = await server.executeOperation({ query, variables });
|
|
61
|
-
const resultText = result?.body?.singleResult?.data?.[name]?.result || result?.body?.singleResult?.errors?.[0]?.message || "";
|
|
62
84
|
|
|
85
|
+
// if we're streaming and there are errors, we return a standard error code
|
|
86
|
+
if (Boolean(req.body.stream)) {
|
|
87
|
+
if (result?.body?.singleResult?.errors) {
|
|
88
|
+
return `[ERROR] ${result.body.singleResult.errors[0].message.split(';')[0]}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// otherwise errors can just be returned as a string
|
|
93
|
+
const resultText = result?.body?.singleResult?.data?.[name]?.result || result?.body?.singleResult?.errors?.[0]?.message || "";
|
|
63
94
|
return resultText;
|
|
64
95
|
};
|
|
65
96
|
|
|
@@ -86,7 +117,6 @@ const processIncomingStream = (requestId, res, jsonResponse, pathway) => {
|
|
|
86
117
|
|
|
87
118
|
// If we haven't sent the stop message yet, do it now
|
|
88
119
|
if (jsonResponse.choices?.[0]?.finish_reason !== "stop") {
|
|
89
|
-
|
|
90
120
|
let jsonEndStream = JSON.parse(JSON.stringify(jsonResponse));
|
|
91
121
|
|
|
92
122
|
if (jsonEndStream.object === 'text_completion') {
|
|
@@ -116,7 +146,6 @@ const processIncomingStream = (requestId, res, jsonResponse, pathway) => {
|
|
|
116
146
|
}
|
|
117
147
|
|
|
118
148
|
const fillJsonResponse = (jsonResponse, inputText, _finishReason) => {
|
|
119
|
-
|
|
120
149
|
jsonResponse.choices[0].finish_reason = null;
|
|
121
150
|
if (jsonResponse.object === 'text_completion') {
|
|
122
151
|
jsonResponse.choices[0].text = inputText;
|
|
@@ -129,6 +158,14 @@ const processIncomingStream = (requestId, res, jsonResponse, pathway) => {
|
|
|
129
158
|
|
|
130
159
|
startStream(res);
|
|
131
160
|
|
|
161
|
+
// If the requestId is an error message, we can't continue
|
|
162
|
+
if (requestId.startsWith('[ERROR]')) {
|
|
163
|
+
fillJsonResponse(jsonResponse, requestId, "stop");
|
|
164
|
+
sendStreamData(jsonResponse);
|
|
165
|
+
finishStream(res, jsonResponse);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
132
169
|
let subscription;
|
|
133
170
|
|
|
134
171
|
subscription = pubsub.subscribe('REQUEST_PROGRESS', (data) => {
|
|
@@ -261,7 +298,14 @@ function buildRestEndpoints(pathways, app, server, config) {
|
|
|
261
298
|
// Create OpenAI compatible endpoints
|
|
262
299
|
app.post('/v1/completions', async (req, res) => {
|
|
263
300
|
const modelName = req.body.model || 'gpt-3.5-turbo';
|
|
264
|
-
|
|
301
|
+
let pathwayName;
|
|
302
|
+
|
|
303
|
+
if (modelName.startsWith('ollama-')) {
|
|
304
|
+
pathwayName = 'sys_ollama_completion';
|
|
305
|
+
req.body.ollamaModel = modelName.replace('ollama-', '');
|
|
306
|
+
} else {
|
|
307
|
+
pathwayName = openAICompletionModels[modelName] || openAICompletionModels['*'];
|
|
308
|
+
}
|
|
265
309
|
|
|
266
310
|
if (!pathwayName) {
|
|
267
311
|
res.status(404).json({
|
|
@@ -297,7 +341,6 @@ function buildRestEndpoints(pathways, app, server, config) {
|
|
|
297
341
|
if (Boolean(req.body.stream)) {
|
|
298
342
|
jsonResponse.id = `cmpl-${resultText}`;
|
|
299
343
|
jsonResponse.choices[0].finish_reason = null;
|
|
300
|
-
//jsonResponse.object = "text_completion.chunk";
|
|
301
344
|
|
|
302
345
|
processIncomingStream(resultText, res, jsonResponse, pathway);
|
|
303
346
|
} else {
|
|
@@ -309,7 +352,14 @@ function buildRestEndpoints(pathways, app, server, config) {
|
|
|
309
352
|
|
|
310
353
|
app.post('/v1/chat/completions', async (req, res) => {
|
|
311
354
|
const modelName = req.body.model || 'gpt-3.5-turbo';
|
|
312
|
-
|
|
355
|
+
let pathwayName;
|
|
356
|
+
|
|
357
|
+
if (modelName.startsWith('ollama-')) {
|
|
358
|
+
pathwayName = 'sys_ollama_chat';
|
|
359
|
+
req.body.ollamaModel = modelName.replace('ollama-', '');
|
|
360
|
+
} else {
|
|
361
|
+
pathwayName = openAIChatModels[modelName] || openAIChatModels['*'];
|
|
362
|
+
}
|
|
313
363
|
|
|
314
364
|
if (!pathwayName) {
|
|
315
365
|
res.status(404).json({
|
|
@@ -364,8 +414,11 @@ function buildRestEndpoints(pathways, app, server, config) {
|
|
|
364
414
|
app.get('/v1/models', async (req, res) => {
|
|
365
415
|
const openAIModels = { ...openAIChatModels, ...openAICompletionModels };
|
|
366
416
|
const defaultModelId = 'gpt-3.5-turbo';
|
|
417
|
+
let models = [];
|
|
367
418
|
|
|
368
|
-
|
|
419
|
+
// Get standard OpenAI-compatible models, filtering out our internal pathway models
|
|
420
|
+
models = Object.entries(openAIModels)
|
|
421
|
+
.filter(([modelId]) => !['ollama-chat', 'ollama-completion'].includes(modelId))
|
|
369
422
|
.map(([modelId]) => {
|
|
370
423
|
if (modelId.includes('*')) {
|
|
371
424
|
modelId = defaultModelId;
|
|
@@ -376,7 +429,16 @@ function buildRestEndpoints(pathways, app, server, config) {
|
|
|
376
429
|
owned_by: 'openai',
|
|
377
430
|
permission: '',
|
|
378
431
|
};
|
|
379
|
-
})
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Get Ollama models if configured
|
|
435
|
+
if (config.get('ollamaUrl')) {
|
|
436
|
+
const ollamaModels = await getOllamaModels(config.get('ollamaUrl'));
|
|
437
|
+
models = [...models, ...ollamaModels];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Filter out duplicates and sort
|
|
441
|
+
models = models
|
|
380
442
|
.filter((model, index, self) => {
|
|
381
443
|
return index === self.findIndex((m) => m.id === model.id);
|
|
382
444
|
})
|