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.

330 lines (307 loc) 15.7 kB
// sys_tool_cognitive_search.js // Tool pathway that handles cognitive search across various indexes import { callPathway } from '../../../../lib/pathwayTools.js'; import { Prompt } from '../../../../server/prompt.js'; import logger from '../../../../lib/logger.js'; import { getSearchResultId } from '../../../../lib/util.js'; export default { prompt: [], useInputChunking: false, enableDuplicateRequests: false, inputParameters: { text: '', filter: '', top: 50, titleOnly: false, stream: false, indexName: '' }, timeout: 300, toolDefinition: [ { type: "function", icon: "📂", function: { name: "SearchPersonalIndex", description: "Search through the user's index of personal documents and indexed uploaded files and retrieve the content of the files. Use this tool if the user refers to a file or a document that you don't see uploaded elsewhere in your context. Some file types (e.g. Word documents, Excel documents, very large files, etc.) cannot be attached to a message and will be chunked and indexed and stored in the personal index.", parameters: { type: "object", properties: { text: { type: "string", description: "The search query to find relevant content in personal documents. Can be a specific phrase or '*' for all documents, or a query formatted with AI Search syntax." }, filter: { type: "string", description: "Optional OData filter expression for date filtering (e.g. 'date ge 2024-02-22T00:00:00Z')" }, top: { type: "integer", description: "Number of results to return (default is 50)" }, titleOnly: { type: "boolean", description: "If true, only return document titles without content - faster and great for counting results" }, userMessage: { type: "string", description: "A user-friendly message that describes what you're doing with this tool" } }, required: ["text", "userMessage"] } } }, { type: "function", icon: "📰", function: { name: "SearchAJA", description: "Search Al Jazeera Arabic news articles. Use this for finding Arabic news content including the latest news and articles. Make sure to include a date filter when looking for recent articles.", parameters: { type: "object", properties: { text: { type: "string", description: "The search query in Arabic to find relevant news articles. Can be a specific phrase or '*' for all articles, or a query formatted with AI Search syntax." }, filter: { type: "string", description: "Optional OData filter expression for date filtering (e.g. 'date ge 2024-02-22T00:00:00Z')" }, top: { type: "integer", description: "Number of results to return (default is 50)" }, titleOnly: { type: "boolean", description: "If true, only return article titles without content - faster and great for counting results" }, userMessage: { type: "string", description: "A user-friendly message that describes what you're doing with this tool" } }, required: ["text", "userMessage"] } } }, { type: "function", icon: "📰", function: { name: "SearchAJE", description: "Search Al Jazeera English news articles. Use this for finding English news content including the latest news and articles. Make sure to include a date filter when looking for recent articles.", parameters: { type: "object", properties: { text: { type: "string", description: "The search query in English to find relevant news articles. Can be a specific phrase or '*' for all articles, or a query formatted with AI Search syntax." }, filter: { type: "string", description: "Optional OData filter expression for date filtering (e.g. 'date ge 2024-02-22T00:00:00Z')" }, top: { type: "integer", description: "Number of results to return (default is 50)" }, titleOnly: { type: "boolean", description: "If true, only return article titles without content - faster and great for counting results" }, userMessage: { type: "string", description: "A user-friendly message that describes what you're doing with this tool" } }, required: ["text", "userMessage"] } } }, { type: "function", icon: "⚡️", function: { name: "SearchWires", description: "Search news wires from Reuters, AFP, AP, and other news agencies. Use this for finding the latest news and articles from the wires. Make sure to include a date filter when looking for recent articles.", parameters: { type: "object", properties: { text: { type: "string", description: "The search query to find relevant news wires. Can be a specific phrase or '*' for all wires, or a query formatted with AI Search syntax." }, filter: { type: "string", description: "Optional OData filter expression for date filtering (e.g. 'date ge 2024-02-22T00:00:00Z')" }, top: { type: "integer", description: "Number of results to return (default is 50)" }, titleOnly: { type: "boolean", description: "If true, only return wire titles without content - faster and great for counting results" }, userMessage: { type: "string", description: "A user-friendly message that describes what you're doing with this tool" } }, required: ["text", "userMessage"] } } } ], executePathway: async ({args, runAllPrompts, resolver}) => { const { text, filter, top, titleOnly, stream, chatId, indexName, semanticConfiguration } = args; // Map tool names to index names const toolToIndex = { 'searchpersonalindex': 'indexcortex', 'searchaja': 'indexucmsaja', 'searchaje': 'indexucmsaje', 'searchwires': 'indexwires' }; // Helper function to remove vector fields from search results const removeVectorFields = (result) => { const { text_vector, image_vector, ...cleanResult } = result; return cleanResult; }; // Helper function to check if error is related to date filter format const hasDateFilterError = (errorMessage) => { if (typeof errorMessage !== 'string') return false; return ( (errorMessage.includes('unsupported data type') && errorMessage.includes('Date')) || errorMessage.includes('date ge') || errorMessage.includes('date filter') ); }; // Helper function to get recovery message based on error type const getRecoveryMessage = (errorMessage, filter) => { if (hasDateFilterError(errorMessage) && filter) { return `The date filter format is incorrect. Azure Cognitive Search requires dates in ISO 8601 format with time (e.g., 'date ge 2025-11-25T00:00:00Z' instead of 'date ge 2025-11-25'). Please adjust the filter parameter and try again, or try without a date filter.`; } return "This tool failed. You can try again or try the backup tool for this function if one is available."; }; // Get the tool name from the function call const toolName = args.toolFunction; const toolIndexName = indexName || toolToIndex[toolName]; if (!toolName || !toolIndexName) { throw new Error(`Invalid tool name: ${toolName}. Search not allowed.`); } try { // Call the cognitive search pathway const response = await callPathway('cognitive_search', { ...args, text, filter, top: top || 50, titleOnly: titleOnly || false, indexName: toolIndexName, semanticConfiguration, stream: stream || false, chatId }, resolver); // Check for errors in resolver if (resolver.errors && resolver.errors.length > 0) { const errorMessages = Array.isArray(resolver.errors) ? resolver.errors.map(err => err.message || err) : [resolver.errors.message || resolver.errors]; const errorMessageStr = errorMessages.join('; '); const recoveryMessage = getRecoveryMessage(errorMessageStr, filter); logger.error(`Cognitive search error for index ${toolIndexName}: ${errorMessageStr}`); return JSON.stringify({ error: errorMessageStr, recoveryMessage: recoveryMessage }); } // Check if response is null or empty if (!response) { const errorMessage = `No response received from cognitive search for index ${toolIndexName}`; logger.error(errorMessage); const recoveryMessage = getRecoveryMessage(errorMessage, filter); return JSON.stringify({ error: errorMessage, recoveryMessage: recoveryMessage }); } // Parse the response let parsedResponse; try { parsedResponse = JSON.parse(response); } catch (parseError) { const errorMessage = `Invalid response format from cognitive search: ${parseError.message}`; logger.error(`Failed to parse cognitive search response for index ${toolIndexName}: ${parseError.message}`); const recoveryMessage = getRecoveryMessage(errorMessage, filter); return JSON.stringify({ error: errorMessage, recoveryMessage: recoveryMessage }); } // Check if parsed response indicates an error if (parsedResponse.error || parsedResponse.Error) { const errorMsg = parsedResponse.error?.message || parsedResponse.Error?.message || parsedResponse.error || parsedResponse.Error || 'Unknown error from cognitive search'; logger.error(`Cognitive search API error for index ${toolIndexName}: ${errorMsg}`); const recoveryMessage = getRecoveryMessage(errorMsg, filter); return JSON.stringify({ error: errorMsg, recoveryMessage: recoveryMessage }); } const combinedResults = []; // Add OData context and count information if present if (parsedResponse["@odata.context"]) { combinedResults.push({ searchResultId: getSearchResultId(), key: "@odata.context", content: parsedResponse["@odata.context"], source_type: 'metadata' }); } if (parsedResponse["@odata.count"]) { combinedResults.push({ searchResultId: getSearchResultId(), key: "@odata.count", content: parsedResponse["@odata.count"].toString(), source_type: 'metadata' }); } if (parsedResponse.value && Array.isArray(parsedResponse.value)) { // Filter out vector fields from each result before adding to combinedResults combinedResults.push(...parsedResponse.value.map(result => ({ ...removeVectorFields(result), searchResultId: getSearchResultId() }))); } // Extract semantic answers const answers = parsedResponse["@search.answers"]; if (answers && Array.isArray(answers)) { const formattedAnswers = answers.map(ans => ({ // Create a pseudo-document structure for answers searchResultId: getSearchResultId(), title: "", // no title for answers content: ans.text || "", // Use text as content key: ans.key, // Keep the key if needed later score: ans.score, // Keep score source_type: 'answer' // Add a type identifier // url: null - Answers don't have URLs })); combinedResults.push(...formattedAnswers); } return JSON.stringify({ _type: "SearchResponse", value: combinedResults }); } catch (e) { const errorMessage = e?.message || e?.toString() || String(e); logger.error(`Error in cognitive search for index ${toolIndexName}: ${errorMessage}`); const recoveryMessage = getRecoveryMessage(errorMessage, filter); // Return error response instead of throwing so agent can see and adjust return JSON.stringify({ error: errorMessage, recoveryMessage: recoveryMessage }); } } };