@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.
Files changed (44) hide show
  1. package/README.md +64 -0
  2. package/config.js +26 -1
  3. package/helper-apps/cortex-realtime-voice-server/src/cortex/memory.ts +2 -2
  4. package/helper-apps/cortex-realtime-voice-server/src/realtime/client.ts +9 -4
  5. package/helper-apps/cortex-realtime-voice-server/src/realtime/realtimeTypes.ts +1 -0
  6. package/lib/util.js +5 -25
  7. package/package.json +5 -2
  8. package/pathways/system/entity/memory/shared/sys_memory_helpers.js +228 -0
  9. package/pathways/system/entity/memory/sys_memory_format.js +30 -0
  10. package/pathways/system/entity/memory/sys_memory_manager.js +85 -27
  11. package/pathways/system/entity/memory/sys_memory_process.js +154 -0
  12. package/pathways/system/entity/memory/sys_memory_required.js +4 -2
  13. package/pathways/system/entity/memory/sys_memory_topic.js +22 -0
  14. package/pathways/system/entity/memory/sys_memory_update.js +50 -150
  15. package/pathways/system/entity/memory/sys_read_memory.js +67 -69
  16. package/pathways/system/entity/memory/sys_save_memory.js +1 -1
  17. package/pathways/system/entity/memory/sys_search_memory.js +1 -1
  18. package/pathways/system/entity/sys_entity_start.js +9 -6
  19. package/pathways/system/entity/sys_generator_image.js +5 -41
  20. package/pathways/system/entity/sys_generator_memory.js +3 -1
  21. package/pathways/system/entity/sys_generator_reasoning.js +1 -1
  22. package/pathways/system/entity/sys_router_tool.js +3 -4
  23. package/pathways/system/rest_streaming/sys_claude_35_sonnet.js +1 -1
  24. package/pathways/system/rest_streaming/sys_claude_3_haiku.js +1 -1
  25. package/pathways/system/rest_streaming/sys_google_gemini_chat.js +1 -1
  26. package/pathways/system/rest_streaming/sys_ollama_chat.js +21 -0
  27. package/pathways/system/rest_streaming/sys_ollama_completion.js +14 -0
  28. package/pathways/system/rest_streaming/sys_openai_chat_o1.js +1 -1
  29. package/pathways/system/rest_streaming/sys_openai_chat_o3_mini.js +1 -1
  30. package/pathways/transcribe_gemini.js +525 -0
  31. package/server/modelExecutor.js +8 -0
  32. package/server/pathwayResolver.js +13 -8
  33. package/server/plugins/claude3VertexPlugin.js +150 -18
  34. package/server/plugins/gemini15ChatPlugin.js +90 -1
  35. package/server/plugins/gemini15VisionPlugin.js +16 -3
  36. package/server/plugins/modelPlugin.js +12 -9
  37. package/server/plugins/ollamaChatPlugin.js +158 -0
  38. package/server/plugins/ollamaCompletionPlugin.js +147 -0
  39. package/server/rest.js +70 -8
  40. package/tests/claude3VertexToolConversion.test.js +411 -0
  41. package/tests/memoryfunction.test.js +560 -46
  42. package/tests/multimodal_conversion.test.js +169 -0
  43. package/tests/openai_api.test.js +332 -0
  44. 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
- const pathwayName = openAICompletionModels[modelName] || openAICompletionModels['*'];
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
- const pathwayName = openAIChatModels[modelName] || openAIChatModels['*'];
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
- const models = Object.entries(openAIModels)
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
  })