@aj-archipelago/cortex
Version:
Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.
429 lines (366 loc) • 20.7 kB
JavaScript
// executeWorkspace.js
// Handles the executeWorkspace GraphQL query resolver and related functionality
//
// This module contains the implementation of the executeWorkspace resolver, which is responsible
// for executing user-defined pathways (workspaces) with various execution modes:
// - Sequential execution of all prompts (default)
// - Parallel execution of specific named prompts
// - Parallel execution of all prompts (wildcard mode)
//
// The resolver supports both legacy pathway formats and new dynamic pathways with cortexPathwayName.
import { v4 as uuidv4 } from 'uuid';
import logger from '../lib/logger.js';
import { callPathway } from '../lib/pathwayTools.js';
import { getPathwayTypeDef, userPathwayInputParameters } from './typeDef.js';
// Helper function to resolve file hashes and add them to chatHistory
const resolveAndAddFileContent = async (pathways, pathwayArgs, requestId, config) => {
let fileContentAdded = false;
// Check if any pathway has file hashes
const pathwaysWithFiles = Array.isArray(pathways) ? pathways : [pathways];
for (const pathway of pathwaysWithFiles) {
if (pathway.fileHashes && pathway.fileHashes.length > 0) {
try {
const { resolveFileHashesToContent } = await import('../lib/fileUtils.js');
const fileContent = await resolveFileHashesToContent(pathway.fileHashes, config, pathwayArgs?.contextId || null);
// Add file content to chatHistory if not already present (only do this once)
if (!fileContentAdded) {
// Initialize chatHistory if it doesn't exist
if (!pathwayArgs.chatHistory) {
pathwayArgs.chatHistory = [];
}
// Find the last user message or create one
let lastUserMessage = null;
for (let i = pathwayArgs.chatHistory.length - 1; i >= 0; i--) {
if (pathwayArgs.chatHistory[i].role === 'user') {
lastUserMessage = pathwayArgs.chatHistory[i];
break;
}
}
if (!lastUserMessage) {
lastUserMessage = {
role: 'user',
content: []
};
pathwayArgs.chatHistory.push(lastUserMessage);
}
// Ensure content is an array
if (!Array.isArray(lastUserMessage.content)) {
lastUserMessage.content = [
JSON.stringify({
type: "text",
text: lastUserMessage.content || ""
})
];
}
// Add file content
lastUserMessage.content.push(...fileContent);
fileContentAdded = true;
}
} catch (error) {
logger.error(`[${requestId}] Failed to resolve file hashes for pathway ${pathway.name || 'unnamed'}: ${error.message}`);
// Continue execution without files
}
// Only process files once for multiple pathways
if (fileContentAdded) break;
}
}
return fileContentAdded;
};
// Helper function to execute pathway with cortex pathway name or fallback to legacy
const executePathwayWithFallback = async (pathway, pathwayArgs, contextValue, info, requestId, originalPrompt = null, config) => {
// Extract cortexPathwayName from originalPrompt (could be original object or transformed Prompt object)
const cortexPathwayName = (originalPrompt && typeof originalPrompt === 'object' && originalPrompt.cortexPathwayName)
? originalPrompt.cortexPathwayName
: null;
if (cortexPathwayName) {
// Use the specific cortex pathway
// Transform parameters for cortex pathway
// Spread all pathway args first, then override specific fields
const cortexArgs = {
...pathwayArgs, // Spread all pathway args
model: pathway.model || pathwayArgs.model || "labeeb-agent", // Use pathway model or default
chatHistory: pathwayArgs.chatHistory ? JSON.parse(JSON.stringify(pathwayArgs.chatHistory)) : [],
systemPrompt: pathway.systemPrompt || pathwayArgs.systemPrompt
};
// Transform old parameters to new format for run_workspace_agent
if (cortexPathwayName === 'run_workspace_agent') {
// Remove old aiStyle parameter (no longer used)
delete cortexArgs.aiStyle;
// Extract researchMode from originalPrompt if not already in pathwayArgs
// originalPrompt could be the original object from JSON or a transformed Prompt object
if (originalPrompt && typeof originalPrompt === 'object' && originalPrompt.researchMode !== undefined) {
cortexArgs.researchMode = originalPrompt.researchMode;
}
// Transform context parameters to agentContext array format (only if agentContext not already provided)
if (!cortexArgs.agentContext && (cortexArgs.contextId || cortexArgs.contextKey || cortexArgs.altContextId || cortexArgs.altContextKey)) {
const agentContext = [];
// Add primary context if present
if (cortexArgs.contextId) {
agentContext.push({
contextId: cortexArgs.contextId,
contextKey: cortexArgs.contextKey || null,
default: true
});
}
// Add alternate context if present
if (cortexArgs.altContextId) {
agentContext.push({
contextId: cortexArgs.altContextId,
contextKey: cortexArgs.altContextKey || null,
default: false
});
}
// If we have at least one context, set agentContext and remove old params
if (agentContext.length > 0) {
cortexArgs.agentContext = agentContext;
delete cortexArgs.contextId;
delete cortexArgs.contextKey;
delete cortexArgs.altContextId;
delete cortexArgs.altContextKey;
}
}
// Ensure researchMode defaults to false if not provided
if (cortexArgs.researchMode === undefined) {
cortexArgs.researchMode = false;
}
}
// If we have text parameter, we need to add it to the chatHistory
if (pathwayArgs.text) {
// Find the last user message or create a new one
let lastUserMessage = null;
for (let i = cortexArgs.chatHistory.length - 1; i >= 0; i--) {
if (cortexArgs.chatHistory[i].role === 'user') {
lastUserMessage = cortexArgs.chatHistory[i];
break;
}
}
if (lastUserMessage) {
// Ensure content is an array
if (!Array.isArray(lastUserMessage.content)) {
lastUserMessage.content = [JSON.stringify({
type: "text",
text: lastUserMessage.content || ""
})];
}
// Add the text parameter as a text content item
const textFromPrompt = originalPrompt?.prompt || pathwayArgs.text;
lastUserMessage.content.unshift(JSON.stringify({
type: "text",
text: `${pathwayArgs.text}\n\n${textFromPrompt}`
}));
} else {
// Create new user message with text
const textFromPrompt = originalPrompt?.prompt || pathwayArgs.text;
cortexArgs.chatHistory.push({
role: 'user',
content: [JSON.stringify({
type: "text",
text: `${pathwayArgs.text}\n\n${textFromPrompt}`
})]
});
}
}
// Create a pathwayResolver to capture extended data like artifacts
const { PathwayResolver } = await import('./pathwayResolver.js');
const cortexPathway = config.get(`pathways.${cortexPathwayName}`);
if (!cortexPathway) {
throw new Error(`Cortex pathway ${cortexPathwayName} not found`);
}
const pathwayResolver = new PathwayResolver({
config,
pathway: cortexPathway,
args: cortexArgs
});
const result = await callPathway(cortexPathwayName, cortexArgs, pathwayResolver);
// Extract resultData from pathwayResolver (includes artifacts and other extended data)
const resultData = pathwayResolver.pathwayResultData
? JSON.stringify(pathwayResolver.pathwayResultData)
: null;
// Return result with extended data
return {
result,
resultData,
warnings: pathwayResolver.warnings,
errors: pathwayResolver.errors
};
} else {
// Fallback to original pathway execution for legacy prompts
const pathwayContext = { ...contextValue, pathway };
return await pathway.rootResolver(null, pathwayArgs, pathwayContext, info);
}
};
// Main executeWorkspace resolver
export const executeWorkspaceResolver = async (_, args, contextValue, info, config, pathwayManager) => {
const startTime = Date.now();
const requestId = uuidv4();
const { userId, pathwayName, promptNames, ...pathwayArgs } = args;
logger.info(`>>> [${requestId}] executeWorkspace started - userId: ${userId}, pathwayName: ${pathwayName}, promptNames: ${promptNames?.join(',') || 'none'}`);
try {
contextValue.config = config;
// Get the base pathway from the user
const pathways = await pathwayManager.getLatestPathways();
if (!pathways[userId] || !pathways[userId][pathwayName]) {
const error = new Error(`Pathway '${pathwayName}' not found for user '${userId}'`);
logger.error(`!!! [${requestId}] ${error.message} - Available users: ${Object.keys(pathways).join(', ')}`);
throw error;
}
const basePathway = pathways[userId][pathwayName];
// If promptNames is specified, use getPathways to get individual pathways and execute in parallel
if (promptNames && promptNames.length > 0) {
// Check if the prompts are in legacy format (array of strings)
// If so, we can't use promptNames filtering and need to ask user to republish
if (pathwayManager.isLegacyPromptFormat(userId, pathwayName)) {
const error = new Error(
`The pathway '${pathwayName}' uses legacy prompt format (array of strings) which doesn't support the promptNames parameter. ` +
`Please unpublish and republish your workspace to upgrade to the new format that supports named prompts.`
);
logger.error(`!!! [${requestId}] ${error.message}`);
throw error;
}
// Handle wildcard case - execute all prompts in parallel
if (promptNames.includes('*')) {
logger.info(`[${requestId}] Executing all prompts in parallel (wildcard specified)`);
const individualPathways = await pathwayManager.getPathways(basePathway);
if (individualPathways.length === 0) {
const error = new Error(`No prompts found in pathway '${pathwayName}'`);
logger.error(`!!! [${requestId}] ${error.message}`);
throw error;
}
// Resolve file content for any pathways that have file hashes
await resolveAndAddFileContent(individualPathways, pathwayArgs, requestId, config);
// Execute all pathways in parallel
const results = await Promise.all(
individualPathways.map(async (pathway, index) => {
try {
// Check if the prompt has a cortexPathwayName (new format)
const originalPrompt = basePathway.prompt[index];
const result = await executePathwayWithFallback(pathway, pathwayArgs, contextValue, info, requestId, originalPrompt, config);
return {
result: result.result,
resultData: result.resultData,
warnings: result.warnings,
errors: result.errors,
promptName: pathway.name || `prompt_${index + 1}`
};
} catch (error) {
logger.error(`!!! [${requestId}] Error in pathway ${index + 1}/${individualPathways.length}: ${pathway.name || 'unnamed'} - ${error.message}`);
throw error;
}
})
);
const duration = Date.now() - startTime;
logger.info(`<<< [${requestId}] executeWorkspace completed successfully in ${duration}ms - returned ${results.length} results`);
// Return a single result with JSON stringified array of results
return {
debug: `Executed ${results.length} prompts in parallel`,
result: JSON.stringify(results),
resultData: null,
previousResult: null,
warnings: [],
errors: [],
contextId: requestId,
tool: 'executeWorkspace'
};
} else {
// Handle specific prompt names
logger.info(`[${requestId}] Executing specific prompts: ${promptNames.join(', ')}`);
const individualPathways = await pathwayManager.getPathways(basePathway, promptNames);
if (individualPathways.length === 0) {
const error = new Error(`No prompts found matching the specified names: ${promptNames.join(', ')}`);
logger.error(`!!! [${requestId}] ${error.message}`);
throw error;
}
// Resolve file content for any pathways that have file hashes
await resolveAndAddFileContent(individualPathways, pathwayArgs, requestId, config);
// Execute all pathways in parallel
const results = await Promise.all(
individualPathways.map(async (pathway, index) => {
try {
// Find the original prompt by name to get the cortexPathwayName
const originalPrompt = basePathway.prompt.find(p =>
(typeof p === 'object' && p.name === pathway.name) ||
(typeof p === 'string' && pathway.name === `prompt_${basePathway.prompt.indexOf(p)}`)
);
const result = await executePathwayWithFallback(pathway, pathwayArgs, contextValue, info, requestId, originalPrompt, config);
return {
result: result.result,
resultData: result.resultData,
warnings: result.warnings,
errors: result.errors,
promptName: pathway.name || `prompt_${index + 1}`
};
} catch (error) {
logger.error(`!!! [${requestId}] Error in pathway ${index + 1}/${individualPathways.length}: ${pathway.name || 'unnamed'} - ${error.message}`);
throw error;
}
})
);
const duration = Date.now() - startTime;
logger.info(`<<< [${requestId}] executeWorkspace completed successfully in ${duration}ms - returned ${results.length} results`);
// Return a single result with JSON stringified array of results (consistent with wildcard case)
return {
debug: `Executed ${results.length} specific prompts in parallel: ${promptNames.join(', ')}`,
result: JSON.stringify(results),
resultData: null,
previousResult: null,
warnings: [],
errors: [],
contextId: requestId,
tool: 'executeWorkspace'
};
}
}
// Default behavior: execute all prompts in sequence
logger.info(`[${requestId}] Executing prompts in sequence`);
const userPathway = await pathwayManager.getPathway(userId, pathwayName);
contextValue.pathway = userPathway;
// Handle file hashes if present in the pathway
await resolveAndAddFileContent(userPathway, pathwayArgs, requestId, config);
// Check if any prompt has cortexPathwayName (for dynamic pathways)
let result;
if (userPathway.prompt && Array.isArray(userPathway.prompt)) {
const firstPrompt = userPathway.prompt[0];
result = await executePathwayWithFallback(userPathway, pathwayArgs, contextValue, info, requestId, firstPrompt, config);
} else {
// No prompt array, use legacy execution
result = await userPathway.rootResolver(null, pathwayArgs, contextValue, info);
}
const duration = Date.now() - startTime;
logger.info(`<<< [${requestId}] executeWorkspace completed successfully in ${duration}ms - returned 1 result`);
return result; // Return single result directly
} catch (error) {
const duration = Date.now() - startTime;
logger.error(`!!! [${requestId}] executeWorkspace failed after ${duration}ms`);
logger.error(`!!! [${requestId}] Error type: ${error.constructor.name}`);
logger.error(`!!! [${requestId}] Error message: ${error.message}`);
logger.error(`!!! [${requestId}] Error stack: ${error.stack}`);
// Log additional context for debugging "memory access out of bounds" errors
if (error.message && error.message.includes('memory')) {
logger.error(`!!! [${requestId}] MEMORY ERROR DETECTED - Additional context:`);
logger.error(`!!! [${requestId}] - Node.js version: ${process.version}`);
logger.error(`!!! [${requestId}] - Memory usage: ${JSON.stringify(process.memoryUsage())}`);
logger.error(`!!! [${requestId}] - Args size estimate: ${JSON.stringify(args).length} chars`);
logger.error(`!!! [${requestId}] - PathwayArgs keys: ${Object.keys(pathwayArgs).join(', ')}`);
}
throw error;
}
};
// Type definitions for executeWorkspace
export const getExecuteWorkspaceTypeDefs = () => {
return `
${getPathwayTypeDef('ExecuteWorkspace', 'String')}
type ExecuteWorkspaceResult {
debug: String
result: String
resultData: String
previousResult: String
warnings: [String]
errors: [String]
contextId: String
tool: String
}
extend type Query {
executeWorkspace(userId: String!, pathwayName: String!, ${userPathwayInputParameters}): ExecuteWorkspaceResult
}
`;
};