UNPKG

@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.

430 lines (366 loc) 16.8 kB
// pathwayTools.js import { encode, decode } from '../lib/encodeCache.js'; import { config } from '../config.js'; import { publishRequestProgress } from "../lib/redisSubscription.js"; import { getSemanticChunks } from "../server/chunker.js"; import logger from '../lib/logger.js'; import { requestState } from '../server/requestState.js'; import { processPathwayParameters } from '../server/typeDef.js'; import { waitForClientToolResult } from '../server/clientToolCallbacks.js'; // callPathway - call a pathway from another pathway const callPathway = async (pathwayName, inArgs, pathwayResolver) => { // Clone the args object to avoid modifying the original const args = JSON.parse(JSON.stringify(inArgs)); const pathway = config.get(`pathways.${pathwayName}`); if (!pathway) { throw new Error(`Pathway ${pathwayName} not found`); } // Merge pathway default parameters with input args, similar to GraphQL typeDef behavior const mergedParams = { ...pathway.defaultInputParameters, ...pathway.inputParameters, ...args }; // Process the merged parameters to convert type specification objects to actual values const processedArgs = processPathwayParameters(mergedParams); const parent = {}; let rootRequestId = pathwayResolver?.rootRequestId || pathwayResolver?.requestId; const contextValue = { config, pathway, requestState }; let data = await pathway.rootResolver(parent, {...processedArgs, rootRequestId}, contextValue ); if (pathwayResolver && contextValue.pathwayResolver) { pathwayResolver.mergeResolver(contextValue.pathwayResolver); } let returnValue = data?.result || null; if (args.async || args.stream) { const { result: requestId } = data; // Fire the resolver for the async requestProgress logger.info(`Callpathway starting async requestProgress, pathway: ${pathwayName}, requestId: ${requestId}`); const { resolver, args } = requestState[requestId]; requestState[requestId].useRedis = false; requestState[requestId].started = true; resolver && await resolver(args); returnValue = null; } return returnValue; }; const callTool = async (toolName, args, toolDefinitions, pathwayResolver) => { let toolResult = null; let toolImages = []; const toolDef = toolDefinitions[toolName.toLowerCase()]; if (!toolDef) { throw new Error(`Tool ${toolName} not found in available tools`); } // Create a sanitized copy of args for logging - only include tool parameters const toolParams = toolDef.definition?.function?.parameters?.properties || {}; const paramKeys = Object.keys(toolParams); const logArgs = {}; // Include only parameters defined in the tool's parameter schema for (const key of paramKeys) { if (args.hasOwnProperty(key)) { const value = args[key]; // Sanitize large objects/arrays if (key === 'chatHistory' || (Array.isArray(value) && value.length > 10)) { logArgs[key] = `[${Array.isArray(value) ? value.length : 'N/A'} items]`; } else if (typeof value === 'object' && value !== null && Object.keys(value).length > 10) { logArgs[key] = `[object with ${Object.keys(value).length} keys]`; } else { logArgs[key] = value; } } } // Also include pathwayParams if they exist (hard-coded tool parameters) if (toolDef.pathwayParams) { Object.assign(logArgs, toolDef.pathwayParams); } logger.debug(`callTool: Starting execution of ${toolName} ${JSON.stringify(logArgs)}`); try { // Check if this is a client-side tool if (toolDef.clientSide === true || toolDef.definition?.clientSide === true) { logger.info(`Tool ${toolName} is a client-side tool - waiting for client execution`); const toolCallbackId = `${toolName}_${Date.now()}_${Math.random().toString(36).substring(7)}`; // Explicitly publish the marker to the stream so the client receives it if (pathwayResolver) { const requestId = pathwayResolver.rootRequestId || pathwayResolver.requestId; const toolCallbackData = { toolUsed: [toolName], clientSideTool: true, toolCallbackName: toolName, toolCallbackId: toolCallbackId, toolCallbackMessage: args.userMessage || `Executing ${toolName}...`, chatId: args.chatId || "", requestId: requestId, // Include requestId so client can submit tool results toolArgs: args }; try { logger.info(`Publishing client-side tool marker to requestId: ${requestId}, toolCallbackId: ${toolCallbackId}`); await publishRequestProgress({ requestId, progress: 0.5, data: JSON.stringify(""), info: JSON.stringify(toolCallbackData) }); } catch (error) { logger.error(`Error publishing client-side tool marker: ${error.message}`); throw error; } // Wait for the client to execute the tool and send back the result logger.info(`Waiting for client tool result: ${toolCallbackId}`); try { // Use 5 minute timeout to accommodate longer operations like CreateApplet const clientResult = await waitForClientToolResult(toolCallbackId, requestId, 300000); logger.info(`Received client tool result for ${toolCallbackId}: ${JSON.stringify(clientResult).substring(0, 200)}`); // If the client reported an error, throw it if (!clientResult.success) { throw new Error(clientResult.error || 'Client tool execution failed'); } // Return the client's result toolResult = typeof clientResult.data === 'string' ? clientResult.data : JSON.stringify(clientResult.data); // Update resolver with tool result pathwayResolver.tool = JSON.stringify({ ...toolCallbackData, result: clientResult.data }); return { result: toolResult, images: [] }; } catch (error) { logger.error(`Error waiting for client tool result: ${error.message}`); throw new Error(`Client tool execution failed: ${error.message}`); } } else { throw new Error('PathwayResolver is required for client-side tools'); } } const pathwayName = toolDef.pathwayName; // Merge hard-coded pathway parameters with runtime args const mergedArgs = { ...(toolDef.pathwayParams || {}), ...args }; if (pathwayName.includes('_generator_')) { toolResult = await callPathway('sys_entity_continue', { ...mergedArgs, generatorPathway: pathwayName, stream: false }, pathwayResolver ); } else { toolResult = await callPathway(pathwayName, mergedArgs, pathwayResolver ); } if (toolResult === null) { return { error: `Tool ${toolName} returned null result` }; } // Handle search results accumulation let parsedResult = null; // Parse the result if it's a string try { parsedResult = typeof toolResult === 'string' ? JSON.parse(toolResult) : toolResult; } catch (e) { // If parsing fails, just return the original result return { result: toolResult, toolImages: toolImages }; } if (pathwayResolver) { // Initialize searchResults array if it doesn't exist if (!pathwayResolver.searchResults) { pathwayResolver.searchResults = []; } // Check if tool result has imageUrl or imageUrls field (for ViewImage/ViewImages tools) if (parsedResult.imageUrl && typeof parsedResult.imageUrl === 'object') { toolImages.push(parsedResult.imageUrl); } if (parsedResult.imageUrls && Array.isArray(parsedResult.imageUrls)) { toolImages.push(...parsedResult.imageUrls); } // Check if this is a search response if (parsedResult._type === "SearchResponse" && Array.isArray(parsedResult.value)) { // Extract and add each search result parsedResult.value.forEach(result => { if (result.searchResultId) { // Extract screenshot if present if (result.screenshot) { toolImages.push(result.screenshot); delete result.screenshot; } // Build content by concatenating headers and chunk if available let content = ''; if (result.header_1) content += result.header_1 + '\n\n'; if (result.header_2) content += result.header_2 + '\n\n'; if (result.header_3) content += result.header_3 + '\n\n'; if (result.chunk) content += result.chunk; // If no headers/chunk were found, fall back to existing content fields if (!content) { content = result.content || result.text || result.chunk || ''; } pathwayResolver.searchResults.push({ searchResultId: result.searchResultId, title: result.title || result.key || '', url: result.url || '', content: content, path: result.path || '', wireid: result.wireid || '', source: result.source || '', slugline: result.slugline || '', date: result.date || '' }); } }); } } const finalResult = { result: parsedResult, toolImages: toolImages }; logger.debug(`callTool: ${toolName} completed successfully, returning: ${JSON.stringify({ hasResult: !!finalResult.result, hasToolImages: !!finalResult.toolImages, toolImagesLength: finalResult.toolImages?.length || 0 })}`); return finalResult; } catch (error) { logger.error(`Error calling tool ${toolName}: ${error.message}`); const errorResult = { error: error.message }; logger.debug(`callTool: ${toolName} failed, returning error: ${JSON.stringify(errorResult)}`); return errorResult; } } const addCitationsToResolver = (pathwayResolver, contentBuffer, directCitations = null) => { if (!pathwayResolver) { return; } // If direct citations are provided, add them directly // This is used by plugins like grokResponsesPlugin that get citations from the API if (directCitations && Array.isArray(directCitations) && directCitations.length > 0) { const pathwayResultData = pathwayResolver.pathwayResultData || {}; pathwayResultData.citations = [...(pathwayResultData.citations || []), ...directCitations]; pathwayResolver.pathwayResultData = pathwayResultData; logger.info(`Adding ${directCitations.length} direct citations to resolver`); } // Also check for :cd_source[id] patterns in content and match against searchResults // Only proceed if there are searchResults to match against if (!pathwayResolver.searchResults) { return; } const regex = /:cd_source\[(.*?)\]/g; let match; const foundIds = []; while ((match = regex.exec(contentBuffer)) !== null) { // Ensure the capture group exists and is not empty if (match[1] && match[1].trim()) { foundIds.push(match[1].trim()); } } if (foundIds.length > 0) { const {searchResults} = pathwayResolver; logger.info(`Found referenced searchResultIds: ${foundIds.join(', ')}`); if (searchResults) { const matchingResults = searchResults.filter(result => foundIds.includes(result.searchResultId)); // Only modify pathwayResultData if we actually found matching results if (matchingResults.length > 0) { const pathwayResultData = pathwayResolver.pathwayResultData || {}; pathwayResultData.citations = [...(pathwayResultData.citations || []), ...matchingResults]; pathwayResolver.pathwayResultData = pathwayResultData; } } } } const gpt3Encode = (text) => { return encode(text); } const gpt3Decode = (text) => { return decode(text); } const say = async (requestId, message, maxMessageLength = Infinity, voiceResponse = true, isEphemeral = true) => { try { const chunks = getSemanticChunks(message, maxMessageLength); const info = JSON.stringify({ ephemeral: isEphemeral, }); for (let chunk of chunks) { await publishRequestProgress({ requestId, progress: 0.5, data: JSON.stringify(chunk), info }); } if (voiceResponse) { await publishRequestProgress({ requestId, progress: 0.5, data: JSON.stringify(" ... "), info }); } await publishRequestProgress({ requestId, progress: 0.5, data: JSON.stringify("\n\n"), info }); } catch (error) { logger.error(`Say error: ${error.message}`); } }; /** * Send a structured tool start message * @param {string} requestId - The request ID * @param {string} toolCallId - Unique identifier for this tool call * @param {string} toolIcon - Icon for the tool (e.g., '🛠️') * @param {string} userMessage - User-friendly message describing what the tool is doing */ const sendToolStart = async (requestId, toolCallId, toolIcon, userMessage) => { try { const info = JSON.stringify({ toolMessage: { type: 'start', callId: toolCallId, icon: toolIcon || '🛠️', userMessage: userMessage } }); await publishRequestProgress({ requestId, progress: 0.5, data: JSON.stringify(""), info }); } catch (error) { logger.error(`sendToolStart error: ${error.message}`); } }; /** * Send a structured tool finish message * @param {string} requestId - The request ID * @param {string} toolCallId - Unique identifier for this tool call (must match the start message) * @param {boolean} success - Whether the tool execution was successful * @param {string} error - Optional error message if success is false */ const sendToolFinish = async (requestId, toolCallId, success, error = null) => { try { const toolMessage = { type: 'finish', callId: toolCallId, success: success }; if (!success && error) { toolMessage.error = error; } const info = JSON.stringify({ toolMessage }); await publishRequestProgress({ requestId, progress: 0.5, data: JSON.stringify(""), info }); } catch (error) { logger.error(`sendToolFinish error: ${error.message}`); } }; export { callPathway, gpt3Encode, gpt3Decode, say, callTool, addCitationsToResolver, sendToolStart, sendToolFinish };