@aj-archipelago/cortex 1.4.31 → 1.4.33

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/config.js CHANGED
@@ -575,6 +575,72 @@ var config = convict({
575
575
  "maxImageSize": 5242880,
576
576
  "supportsStreaming": true
577
577
  },
578
+ "claude-sonnet-4": {
579
+ "type": "CLAUDE-ANTHROPIC",
580
+ "emulateOpenAIChatModel": "claude-sonnet-4",
581
+ "endpoints": [
582
+ {
583
+ "name": "Anthropic Claude Sonnet 4",
584
+ "url": "https://api.anthropic.com/v1/messages",
585
+ "headers": {
586
+ "x-api-key": "{{CLAUDE_API_KEY}}",
587
+ "Content-Type": "application/json"
588
+ },
589
+ "params": {
590
+ "model": "claude-sonnet-4-20250514"
591
+ },
592
+ "requestsPerSecond": 10
593
+ }
594
+ ],
595
+ "maxTokenLength": 200000,
596
+ "maxReturnTokens": 64000,
597
+ "maxImageSize": 31457280,
598
+ "supportsStreaming": true
599
+ },
600
+ "claude-45-sonnet": {
601
+ "type": "CLAUDE-ANTHROPIC",
602
+ "emulateOpenAIChatModel": "claude-4.5-sonnet",
603
+ "endpoints": [
604
+ {
605
+ "name": "Anthropic Claude 4.5 Sonnet",
606
+ "url": "https://api.anthropic.com/v1/messages",
607
+ "headers": {
608
+ "x-api-key": "{{CLAUDE_API_KEY}}",
609
+ "Content-Type": "application/json"
610
+ },
611
+ "params": {
612
+ "model": "claude-sonnet-4-5-20250514"
613
+ },
614
+ "requestsPerSecond": 10
615
+ }
616
+ ],
617
+ "maxTokenLength": 200000,
618
+ "maxReturnTokens": 64000,
619
+ "maxImageSize": 31457280,
620
+ "supportsStreaming": true
621
+ },
622
+ "claude-45-opus": {
623
+ "type": "CLAUDE-ANTHROPIC",
624
+ "emulateOpenAIChatModel": "claude-4.5-opus",
625
+ "endpoints": [
626
+ {
627
+ "name": "Anthropic Claude 4.5 Opus",
628
+ "url": "https://api.anthropic.com/v1/messages",
629
+ "headers": {
630
+ "x-api-key": "{{CLAUDE_API_KEY}}",
631
+ "Content-Type": "application/json"
632
+ },
633
+ "params": {
634
+ "model": "claude-opus-4-5-20250514"
635
+ },
636
+ "requestsPerSecond": 10
637
+ }
638
+ ],
639
+ "maxTokenLength": 200000,
640
+ "maxReturnTokens": 32000,
641
+ "maxImageSize": 31457280,
642
+ "supportsStreaming": true
643
+ },
578
644
  "gemini-flash-25-vision": {
579
645
  "type": "GEMINI-1.5-VISION",
580
646
  "emulateOpenAIChatModel": "gemini-flash-25",
@@ -778,6 +844,12 @@ var config = convict({
778
844
  env: 'OPENAI_API_KEY',
779
845
  sensitive: true
780
846
  },
847
+ claudeApiKey: {
848
+ format: String,
849
+ default: null,
850
+ env: 'CLAUDE_API_KEY',
851
+ sensitive: true
852
+ },
781
853
  openaiApiUrl: {
782
854
  format: String,
783
855
  default: 'https://api.openai.com/v1/completions',
@@ -1,4 +1,4 @@
1
- FROM node:18-alpine
1
+ FROM node:22-alpine
2
2
 
3
3
  WORKDIR /usr/src/app
4
4
 
package/lib/fileUtils.js CHANGED
@@ -1561,11 +1561,27 @@ async function syncAndStripFilesFromChatHistory(chatHistory, agentContext, chatI
1561
1561
 
1562
1562
  // Build lookup map from contextId to contextKey for updates
1563
1563
  const contextKeyMap = new Map(agentContext.map(ctx => [ctx.contextId, ctx.contextKey || null]));
1564
-
1564
+
1565
+ // Helper to normalize URLs by stripping query parameters (SAS tokens, etc.)
1566
+ // This allows matching URLs that have different query params but same base path
1567
+ const normalizeUrl = (url) => {
1568
+ if (!url) return null;
1569
+ try {
1570
+ // Handle GCS URLs (gs://) by keeping them as-is (no query params)
1571
+ if (url.startsWith('gs://')) return url;
1572
+ const parsed = new URL(url);
1573
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
1574
+ } catch {
1575
+ // If URL parsing fails, return as-is
1576
+ return url;
1577
+ }
1578
+ };
1579
+
1565
1580
  // Build lookup maps for fast matching and context lookup (use ALL files, not just filtered)
1566
1581
  // This allows us to find files that exist in Redis but don't have inCollection set yet
1582
+ // URLs are normalized (query params stripped) to handle SAS tokens and other transient params
1567
1583
  const collectionByHash = new Map(allFiles.filter(f => f.hash).map(f => [f.hash, f]));
1568
- const collectionByUrl = new Map(allFiles.filter(f => f.url).map(f => [f.url, f]));
1584
+ const collectionByUrl = new Map(allFiles.filter(f => f.url).map(f => [normalizeUrl(f.url), f]));
1569
1585
  const collectionByGcs = new Map(allFiles.filter(f => f.gcs).map(f => [f.gcs, f]));
1570
1586
 
1571
1587
  // Helper to get file from collection (by hash, URL, or GCS) to find _contextId
@@ -1573,12 +1589,15 @@ async function syncAndStripFilesFromChatHistory(chatHistory, agentContext, chatI
1573
1589
  const fileHash = contentObj.hash;
1574
1590
  const fileUrl = contentObj.url || contentObj.image_url?.url;
1575
1591
  const fileGcs = contentObj.gcs;
1576
-
1592
+
1577
1593
  if (fileHash && collectionByHash.has(fileHash)) {
1578
1594
  return collectionByHash.get(fileHash);
1579
1595
  }
1580
- if (fileUrl && collectionByUrl.has(fileUrl)) {
1581
- return collectionByUrl.get(fileUrl);
1596
+ if (fileUrl) {
1597
+ const normalizedUrl = normalizeUrl(fileUrl);
1598
+ if (normalizedUrl && collectionByUrl.has(normalizedUrl)) {
1599
+ return collectionByUrl.get(normalizedUrl);
1600
+ }
1582
1601
  }
1583
1602
  if (fileGcs && collectionByGcs.has(fileGcs)) {
1584
1603
  return collectionByGcs.get(fileGcs);
@@ -31,7 +31,7 @@ class LocalStorage extends StorageStrategy {
31
31
  const data = await fs.promises.readFile(this.filePath, 'utf8');
32
32
  return JSON.parse(data);
33
33
  } catch (error) {
34
- logger.error(`Error loading pathways from ${this.filePath}:`, error);
34
+ logger.error(`Error loading pathways from ${this.filePath}: ${error.message}`);
35
35
  throw error;
36
36
  }
37
37
  }
@@ -88,7 +88,7 @@ class AzureBlobStorage extends StorageStrategy {
88
88
  logger.info(`Loaded pathways from Azure Blob Storage. ${Object.keys(parsedData).map(user => `${user}(${Object.keys(parsedData[user])})`).join(', ')}`);
89
89
  return parsedData;
90
90
  } catch (error) {
91
- logger.error('Error loading pathways from Azure Blob Storage:', error);
91
+ logger.error(`Error loading pathways from Azure Blob Storage: ${error.message}`);
92
92
  throw error;
93
93
  }
94
94
  }
@@ -99,7 +99,7 @@ class AzureBlobStorage extends StorageStrategy {
99
99
  const content = JSON.stringify(data, null, 2);
100
100
  await blockBlobClient.upload(content, content.length);
101
101
  } catch (error) {
102
- logger.error('Error saving pathways to Azure Blob Storage:', error);
102
+ logger.error(`Error saving pathways to Azure Blob Storage: ${error.message}`);
103
103
  }
104
104
  }
105
105
 
@@ -155,7 +155,7 @@ class S3Storage extends StorageStrategy {
155
155
  const dataString = await streamToString(readableStream);
156
156
  return JSON.parse(dataString);
157
157
  } catch (error) {
158
- logger.error('Error loading pathways from S3:', error);
158
+ logger.error(`Error loading pathways from S3: ${error.message}`);
159
159
  throw error;
160
160
  }
161
161
  }
@@ -170,7 +170,7 @@ class S3Storage extends StorageStrategy {
170
170
  };
171
171
  await this.s3.putObject(params);
172
172
  } catch (error) {
173
- logger.error('Error saving pathways to S3:', error);
173
+ logger.error(`Error saving pathways to S3: ${error.message}`);
174
174
  }
175
175
  }
176
176
 
@@ -349,6 +349,7 @@ class PathwayManager {
349
349
  const promptName = typeof promptItem === 'string' ? defaultName : (promptItem.name || defaultName);
350
350
  const promptFiles = typeof promptItem === 'string' ? [] : (promptItem.files || []);
351
351
  const cortexPathwayName = typeof promptItem === 'string' ? null : (promptItem.cortexPathwayName || null);
352
+ const researchMode = typeof promptItem === 'string' ? undefined : (promptItem.researchMode !== undefined ? promptItem.researchMode : undefined);
352
353
 
353
354
  const messages = [];
354
355
 
@@ -383,6 +384,11 @@ class PathwayManager {
383
384
  prompt.cortexPathwayName = cortexPathwayName;
384
385
  }
385
386
 
387
+ // Preserve researchMode if present
388
+ if (researchMode !== undefined) {
389
+ prompt.researchMode = researchMode;
390
+ }
391
+
386
392
  return prompt;
387
393
  }
388
394
 
@@ -460,6 +466,7 @@ class PathwayManager {
460
466
  prompt: String!
461
467
  files: [String!]
462
468
  cortexPathwayName: String
469
+ researchMode: Boolean
463
470
  }
464
471
 
465
472
  input PathwayInput {
@@ -537,7 +544,7 @@ class PathwayManager {
537
544
 
538
545
  return this.pathways;
539
546
  } catch (error) {
540
- logger.error('Error in getLatestPathways:', error);
547
+ logger.error(`Error in getLatestPathways: ${error.message}`);
541
548
  throw error;
542
549
  }
543
550
  }
@@ -6,6 +6,7 @@ import { getSemanticChunks } from "../server/chunker.js";
6
6
  import logger from '../lib/logger.js';
7
7
  import { requestState } from '../server/requestState.js';
8
8
  import { processPathwayParameters } from '../server/typeDef.js';
9
+ import { waitForClientToolResult } from '../server/clientToolCallbacks.js';
9
10
 
10
11
  // callPathway - call a pathway from another pathway
11
12
  const callPathway = async (pathwayName, inArgs, pathwayResolver) => {
@@ -91,6 +92,76 @@ const callTool = async (toolName, args, toolDefinitions, pathwayResolver) => {
91
92
  logger.debug(`callTool: Starting execution of ${toolName} ${JSON.stringify(logArgs)}`);
92
93
 
93
94
  try {
95
+ // Check if this is a client-side tool
96
+ if (toolDef.clientSide === true || toolDef.definition?.clientSide === true) {
97
+ logger.info(`Tool ${toolName} is a client-side tool - waiting for client execution`);
98
+
99
+ const toolCallbackId = `${toolName}_${Date.now()}_${Math.random().toString(36).substring(7)}`;
100
+
101
+ // Explicitly publish the marker to the stream so the client receives it
102
+ if (pathwayResolver) {
103
+ const requestId = pathwayResolver.rootRequestId || pathwayResolver.requestId;
104
+
105
+ const toolCallbackData = {
106
+ toolUsed: [toolName],
107
+ clientSideTool: true,
108
+ toolCallbackName: toolName,
109
+ toolCallbackId: toolCallbackId,
110
+ toolCallbackMessage: args.userMessage || `Executing ${toolName}...`,
111
+ chatId: args.chatId || "",
112
+ requestId: requestId, // Include requestId so client can submit tool results
113
+ toolArgs: args
114
+ };
115
+
116
+ try {
117
+ logger.info(`Publishing client-side tool marker to requestId: ${requestId}, toolCallbackId: ${toolCallbackId}`);
118
+ await publishRequestProgress({
119
+ requestId,
120
+ progress: 0.5,
121
+ data: JSON.stringify(""),
122
+ info: JSON.stringify(toolCallbackData)
123
+ });
124
+ } catch (error) {
125
+ logger.error(`Error publishing client-side tool marker: ${error.message}`);
126
+ throw error;
127
+ }
128
+
129
+ // Wait for the client to execute the tool and send back the result
130
+ logger.info(`Waiting for client tool result: ${toolCallbackId}`);
131
+ try {
132
+ // Use 5 minute timeout to accommodate longer operations like CreateApplet
133
+ const clientResult = await waitForClientToolResult(toolCallbackId, requestId, 300000);
134
+ logger.info(`Received client tool result for ${toolCallbackId}: ${JSON.stringify(clientResult).substring(0, 200)}`);
135
+
136
+ // If the client reported an error, throw it
137
+ if (!clientResult.success) {
138
+ throw new Error(clientResult.error || 'Client tool execution failed');
139
+ }
140
+
141
+ // Return the client's result
142
+ toolResult = typeof clientResult.data === 'string'
143
+ ? clientResult.data
144
+ : JSON.stringify(clientResult.data);
145
+
146
+ // Update resolver with tool result
147
+ pathwayResolver.tool = JSON.stringify({
148
+ ...toolCallbackData,
149
+ result: clientResult.data
150
+ });
151
+
152
+ return {
153
+ result: toolResult,
154
+ images: []
155
+ };
156
+ } catch (error) {
157
+ logger.error(`Error waiting for client tool result: ${error.message}`);
158
+ throw new Error(`Client tool execution failed: ${error.message}`);
159
+ }
160
+ } else {
161
+ throw new Error('PathwayResolver is required for client-side tools');
162
+ }
163
+ }
164
+
94
165
  const pathwayName = toolDef.pathwayName;
95
166
  // Merge hard-coded pathway parameters with runtime args
96
167
  const mergedArgs = {
@@ -356,4 +427,24 @@ const sendToolFinish = async (requestId, toolCallId, success, error = null) => {
356
427
  }
357
428
  };
358
429
 
359
- export { callPathway, gpt3Encode, gpt3Decode, say, callTool, addCitationsToResolver, sendToolStart, sendToolFinish };
430
+ /**
431
+ * Wrap a promise with a timeout
432
+ * @param {Promise} promise - The promise to wrap
433
+ * @param {number} timeoutMs - Timeout in milliseconds
434
+ * @param {string} errorMessage - Error message if timeout occurs
435
+ * @returns {Promise} - The original promise or rejection on timeout
436
+ */
437
+ const withTimeout = (promise, timeoutMs, errorMessage = 'Operation timed out') => {
438
+ let timeoutId;
439
+ const timeoutPromise = new Promise((_, reject) => {
440
+ timeoutId = setTimeout(() => {
441
+ reject(new Error(errorMessage));
442
+ }, timeoutMs);
443
+ });
444
+
445
+ return Promise.race([promise, timeoutPromise]).finally(() => {
446
+ clearTimeout(timeoutId);
447
+ });
448
+ };
449
+
450
+ export { callPathway, gpt3Encode, gpt3Decode, say, callTool, addCitationsToResolver, sendToolStart, sendToolFinish, withTimeout };
@@ -101,7 +101,16 @@ const createLimiter = (endpoint, name, index) => {
101
101
  logger.debug(`Limiter request cancelled for ${cortexId}-${name}-${index}: Id: ${info.options.id || 'none'}`);
102
102
  endpoint.monitor.incrementErrorCount();
103
103
  } else {
104
- logger.error(`Limiter request failed for ${cortexId}-${name}-${index}: Id: ${info.options.id || 'none'}: ${error?.message || error}`);
104
+ const errorMsg = error?.message || error;
105
+ const status = error?.status || 'unknown';
106
+ logger.error(`Limiter request failed for ${cortexId}-${name}-${index}: Id: ${info.options.id || 'none'}: [${status}] ${errorMsg}`);
107
+ // Log response data if available (helpful for debugging 400 errors)
108
+ if (error?.responseData) {
109
+ const responseDataStr = typeof error.responseData === 'string'
110
+ ? error.responseData.substring(0, 1000)
111
+ : JSON.stringify(error.responseData, null, 2).substring(0, 1000);
112
+ logger.error(`Response data: ${responseDataStr}`);
113
+ }
105
114
  }
106
115
  });
107
116
 
@@ -239,11 +248,46 @@ const requestWithMonitor = async (endpoint, url, data, axiosConfigObj) => {
239
248
  } catch (error) {
240
249
  // throw new error with duration as part of the error data
241
250
  const { code, name } = error;
242
- const finalStatus = error?.response?.status ?? error?.status
243
- const statusText = error?.response?.statusText ?? error?.statusText
244
- const errorMessage = error?.response?.data?.message ?? error?.response?.data?.error?.message ?? error?.message ?? String(error);
251
+ const finalStatus = error?.response?.status ?? error?.status;
252
+ const statusText = error?.response?.statusText ?? error?.statusText;
253
+ let responseData = error?.response?.data;
254
+
255
+ // For streaming requests with errors, the response data is a stream that needs to be consumed
256
+ if (responseData && typeof responseData.on === 'function') {
257
+ try {
258
+ const chunks = [];
259
+ for await (const chunk of responseData) {
260
+ chunks.push(chunk);
261
+ }
262
+ const streamContent = Buffer.concat(chunks).toString('utf-8');
263
+ try {
264
+ responseData = JSON.parse(streamContent);
265
+ } catch {
266
+ responseData = { rawContent: streamContent.substring(0, 2000) };
267
+ }
268
+ } catch (streamError) {
269
+ logger.debug(`Could not read error stream: ${streamError.message}`);
270
+ responseData = null;
271
+ }
272
+ }
273
+
274
+ // Extract error message from various possible locations in the response
275
+ const errorMessage = responseData?.message
276
+ ?? responseData?.error?.message
277
+ ?? responseData?.error?.status
278
+ ?? responseData?.rawContent?.substring(0, 500)
279
+ ?? error?.message
280
+ ?? String(error);
281
+
282
+ // Log full response data for debugging 4xx errors (especially 400)
283
+ if (finalStatus >= 400 && finalStatus < 500 && responseData) {
284
+ const responseDataStr = typeof responseData === 'string'
285
+ ? responseData.substring(0, 2000)
286
+ : JSON.stringify(responseData, null, 2).substring(0, 2000);
287
+ logger.error(`HTTP ${finalStatus} error response data: ${responseDataStr}`);
288
+ }
245
289
 
246
- throw { code, message: errorMessage, status: finalStatus, statusText, name, duration: endpoint?.monitor?.incrementErrorCount(callId, finalStatus) };
290
+ throw { code, message: errorMessage, status: finalStatus, statusText, name, responseData, duration: endpoint?.monitor?.incrementErrorCount(callId, finalStatus) };
247
291
  }
248
292
  let duration;
249
293
  if (response.status >= 200 && response.status < 300) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.4.31",
3
+ "version": "1.4.33",
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": {
@@ -0,0 +1,82 @@
1
+ // sys_compress_context.js
2
+ // Compresses chat history containing tool calls and results while preserving critical information
3
+ // Used when context window is approaching limits to prevent 400 errors
4
+
5
+ import { Prompt } from '../../../server/prompt.js';
6
+ import logger from '../../../lib/logger.js';
7
+
8
+ export default {
9
+ prompt: [
10
+ new Prompt({ messages: [
11
+ {
12
+ "role": "system",
13
+ "content": `You are an AI assistant that compresses conversation history while preserving critical information needed to continue the task.
14
+
15
+ Your job is to create a concise summary from tool calls and their results that:
16
+
17
+ 1. **Preserves the research intent** - What was the user trying to find or accomplish?
18
+
19
+ 2. **Summarizes tool calls and results** - For each tool call, include:
20
+ - The tool name and purpose
21
+ - Key results and findings (especially data, facts, file names, URLs)
22
+ - Important decisions made based on results
23
+
24
+ 3. **CRITICAL: Preserve exact data** - You MUST preserve:
25
+ - Exact numbers, percentages, dollar amounts, dates, statistics
26
+ - ALL URLs exactly as written (never truncate URLs)
27
+ - Source citations (publication names, report numbers, author names)
28
+ - File names and paths exactly as they appear
29
+
30
+ 4. **Maintain citation integrity** - Preserve file names, URLs, source references
31
+
32
+ 5. **Keep it actionable** - What has been accomplished? What still needs to be done?
33
+
34
+ Format as a clear narrative that another AI agent could read to understand the research progress.
35
+
36
+ Be concise but comprehensive. When in doubt, preserve more detail for numbers, URLs, and citations.`
37
+ },
38
+ {
39
+ "role": "user",
40
+ "content": `Please compress the following tool calls and results into a concise research summary:
41
+
42
+ {{{researchContent}}}
43
+
44
+ Provide a clear summary preserving all URLs, citations, and numerical data.`
45
+ }
46
+ ]})
47
+ ],
48
+ inputParameters: {
49
+ researchContent: '',
50
+ language: "English",
51
+ },
52
+ model: 'gemini-flash-3-vision',
53
+ useInputChunking: false,
54
+ enableDuplicateRequests: false,
55
+ timeout: 120,
56
+
57
+ executePathway: async ({args, runAllPrompts}) => {
58
+ try {
59
+ // Extract URLs for validation
60
+ const urls = new Set();
61
+ const content = args.researchContent || '';
62
+ const urlMatches = content.match(/https?:\/\/[^\s\)\]"']+/g);
63
+ if (urlMatches) urlMatches.forEach(url => urls.add(url));
64
+
65
+ const result = await runAllPrompts(args);
66
+
67
+ // Validate URL preservation
68
+ if (urls.size > 0 && typeof result === 'string') {
69
+ const preserved = Array.from(urls).filter(url => result.includes(url));
70
+ const rate = preserved.length / urls.size;
71
+ if (rate < 0.7) {
72
+ logger.warn(`Context compression preserved only ${(rate * 100).toFixed(0)}% of URLs (${preserved.length}/${urls.size})`);
73
+ }
74
+ }
75
+
76
+ return result;
77
+ } catch (error) {
78
+ logger.error(`Error in sys_compress_context: ${error.message}`);
79
+ return `[Compression failed] Previous tool calls have been summarized to save context space.`;
80
+ }
81
+ }
82
+ };