Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
105 lines
4.4 KiB
JavaScript
105 lines
4.4 KiB
JavaScript
/**
|
|
* ADR-091 Phase 2 / Ticket #41161 (AC17): Response Mode Utilities
|
|
*
|
|
* Provides keyword-based relevance checking for agents with
|
|
* `response_mode: 'topic_only'`. When a group conversation has multiple
|
|
* sub-agents, a topic_only agent should only respond when the incoming
|
|
* message appears to be about its area of expertise.
|
|
*
|
|
* Approach: simple keyword extraction + case-insensitive matching.
|
|
* No AI/ML -- just stop-word filtering and substring comparison.
|
|
*/
|
|
|
|
/**
|
|
* Common English stop words that carry no topical signal.
|
|
* Kept as a module-level Set for reuse across calls.
|
|
* @type {Set<string>}
|
|
*/
|
|
const STOP_WORDS = new Set([
|
|
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should',
|
|
'may', 'might', 'can', 'shall', 'and', 'but', 'or', 'nor', 'not', 'so', 'yet',
|
|
'for', 'to', 'of', 'in', 'on', 'at', 'by', 'with', 'from', 'as', 'into',
|
|
'this', 'that', 'these', 'those', 'it', 'its', 'you', 'your', 'i', 'my', 'we', 'our',
|
|
'they', 'their', 'he', 'she', 'him', 'her', 'his', 'if', 'then', 'than', 'when', 'where',
|
|
'how', 'what', 'which', 'who', 'whom', 'all', 'each', 'every', 'both', 'few', 'more',
|
|
'most', 'other', 'some', 'such', 'no', 'only', 'own', 'same', 'just', 'also',
|
|
// Additional generic words that appear in many agent configs
|
|
'agent', 'assistant', 'help', 'respond', 'please', 'always', 'make', 'like',
|
|
'over', 'take', 'very', 'still', 'using', 'about', 'after', 'been', 'there',
|
|
'out', 'one', 'get', 'set', 'use', 'new', 'way', 'any'
|
|
]);
|
|
|
|
/**
|
|
* Extract meaningful keywords from a block of text.
|
|
*
|
|
* Rules:
|
|
* - Strips non-alphanumeric characters (keeps Unicode letters and digits)
|
|
* - Splits on whitespace
|
|
* - Filters out words shorter than 3 characters
|
|
* - Filters out common stop words
|
|
* - Returns de-duplicated, lowercased keywords
|
|
*
|
|
* @param {string} text - Raw text to extract keywords from
|
|
* @returns {string[]} Array of unique, lowercased keywords
|
|
*/
|
|
function extractKeywords(text) {
|
|
if (!text || typeof text !== 'string') return [];
|
|
|
|
return [...new Set(
|
|
text
|
|
.replace(/[^a-zA-Z0-9\u00C0-\u024F\u0400-\u04FF\s]/g, ' ')
|
|
.split(/\s+/)
|
|
.map(w => w.toLowerCase())
|
|
.filter(word => word.length >= 3 && !STOP_WORDS.has(word))
|
|
)];
|
|
}
|
|
|
|
/**
|
|
* Determine whether an incoming message is relevant to an agent's area of
|
|
* expertise. Designed for agents configured with `response_mode: 'topic_only'`.
|
|
*
|
|
* The function collects text from the agent's `name`, `description`,
|
|
* `system_prompt`, `main_instructions`, and `tags`, extracts meaningful
|
|
* keywords, and checks whether the message content contains at least one
|
|
* matching keyword (case-insensitive substring match).
|
|
*
|
|
* @param {string} messageContent - The user's message text
|
|
* @param {Object} agentConfig - Agent configuration object from the AI Agents table
|
|
* @param {string} [agentConfig.name] - Agent display name
|
|
* @param {string} [agentConfig.description] - Agent description / purpose
|
|
* @param {string} [agentConfig.system_prompt] - System prompt text
|
|
* @param {string} [agentConfig.main_instructions] - Main instructions text
|
|
* @param {string|string[]} [agentConfig.tags] - Tags (string or array)
|
|
* @returns {boolean} `true` if the message appears relevant to this agent
|
|
*/
|
|
function isMessageRelevantToAgent(messageContent, agentConfig) {
|
|
if (!messageContent || typeof messageContent !== 'string') return false;
|
|
if (!agentConfig || typeof agentConfig !== 'object') return false;
|
|
|
|
// Collect keyword sources from agent config
|
|
const { name, description, system_prompt, main_instructions, tags } = agentConfig;
|
|
|
|
const sources = [name, description, system_prompt, main_instructions, tags].filter(Boolean);
|
|
|
|
if (sources.length === 0) return false;
|
|
|
|
// Normalise each source (tags may be an array) and join into one text blob
|
|
const configText = sources
|
|
.map(source => (Array.isArray(source) ? source.join(' ') : String(source)))
|
|
.join(' ');
|
|
|
|
const keywords = extractKeywords(configText);
|
|
|
|
if (keywords.length === 0) return false;
|
|
|
|
// Check if message content matches any keyword (case-insensitive)
|
|
const lowerContent = messageContent.toLowerCase();
|
|
const matchScore = keywords.filter(kw => lowerContent.includes(kw)).length;
|
|
|
|
// Threshold: at least 1 keyword match
|
|
return matchScore >= 1;
|
|
}
|
|
|
|
export { isMessageRelevantToAgent, extractKeywords, STOP_WORDS };
|
|
export default isMessageRelevantToAgent;
|