/**
* Actions entity class for GitHub workflow operations
* @file interaction.js
* @license Apache-2.0
* @version 3.0.0
*
* @author Michael Hay <michael.hay@mediumroast.io>
* @copyright 2025 Mediumroast, Inc. All rights reserved.
*/
import { BaseObjects } from '../baseObjects.js';
import { logger } from '../logger.js';
export class Interactions extends BaseObjects {
constructor(token, org, processName) {
super(token, org, processName, 'Interactions');
// Add interaction-specific cache settings
this._cacheKeys.byHash = `${this.objType}_byHash`;
this._cacheKeys.byText = `${this.objType}_byText`;
this._cacheKeys.analysis = `${this.objType}_analysis`;
this._cacheKeys.topics = `${this.objType}_topics`;
this._cacheKeys.similar = `${this.objType}_similar`;
// Set cache timeouts
this.cacheTimeouts.analysis = 600000; // 10 minutes for content analysis
this.cacheTimeouts.topics = 600000; // 10 minutes for topics
this.cacheTimeouts.similar = 600000; // 10 minutes for similarity results
}
/**
* Override deleteObj to handle cross-entity references
* @param {string} objName - Name of the interaction to delete
* @returns {Promise<Array>} Operation result
*/
async deleteObj(objName) {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'deleteObj') :
{ end: () => {} };
try {
const source = {
from: 'Interactions',
to: ['Companies']
};
return await super.deleteObj(objName, source);
} finally {
tracking.end();
}
}
/**
* Find interaction by file hash
* @param {string} hash - File hash to search for
* @returns {Promise<Array>} Found interaction or error
*/
async findByHash(hash) {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'findByHash') :
{ end: () => {} };
try {
// Validate parameters
const validationError = this._validateParams(
{ hash },
{ hash: 'string' }
);
if (validationError) return validationError;
// Use cache with dependencies to container data
const hashCacheKey = `${this._cacheKeys.byHash}_${hash}`;
return await this.cache.getOrFetch(
hashCacheKey,
() => this.findByX('file_hash', hash),
this.cacheTimeouts[this.objType] || 180000,
[this._cacheKeys.container] // Depends on all interactions
);
} finally {
tracking.end();
}
}
/**
* Finds interactions containing specific text in content or metadata
* @param {string} text - The text to search for
* @returns {Promise<Array>} Search results
*/
async findByText(text) {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'findByText') :
{ end: () => {} };
try {
// Validate parameters
const validationError = this._validateParams(
{ text },
{ text: 'string' }
);
if (validationError) return validationError;
// Use cache with text search key
const textCacheKey = `${this._cacheKeys.byText}_${text.toLowerCase()}`;
return await this.cache.getOrFetch(
textCacheKey,
async () => {
const allObjectsResp = await this.getAll();
if (!allObjectsResp[0]) {
return allObjectsResp;
}
const allObjects = allObjectsResp[2].mrJson;
const searchText = text.toLowerCase();
// Search through text fields
const results = allObjects.filter(interaction => {
if (interaction.name?.toLowerCase().includes(searchText)) return true;
if (interaction.abstract?.toLowerCase().includes(searchText)) return true;
if (interaction.description?.toLowerCase().includes(searchText)) return true;
if (interaction.summary?.toLowerCase().includes(searchText)) return true;
return false;
});
if (results.length === 0) {
return this._createError(
`No interactions found containing text: "${text}"`,
null,
404
);
}
return this._createSuccess(
`Found ${results.length} interactions containing text: "${text}"`,
results
);
},
this.cacheTimeouts[this.objType] || 180000,
[this._cacheKeys.container] // Depends on all interactions
);
} catch (error) {
return this._createError(
`Error searching interactions: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
/**
* Gets detailed content analysis for an interaction
* @param {string} name - Interaction name
* @returns {Promise<Array>} Analysis results
*/
async getInteractionAnalysis(name) {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'getInteractionAnalysis') :
{ end: () => {} };
try {
// Validate parameter
const validationError = this._validateParams(
{ name },
{ name: 'string' }
);
if (validationError) return validationError;
// Use cache for analysis results
const analysisCacheKey = `${this._cacheKeys.analysis}_${name}`;
return await this.cache.getOrFetch(
analysisCacheKey,
async () => {
// Find the interaction
const interactionResp = await this.findByName(name);
if (!interactionResp[0]) {
return interactionResp;
}
const interaction = interactionResp[2][0];
// Check if this interaction has content to analyze
if (!interaction.url) {
return this._createError(
`Interaction [${name}] does not have content to analyze`,
null,
400
);
}
// Use transaction pattern for better error handling
return this._executeTransaction([
// Step 1: Read the content
async () => {
try {
const contentResp = await this.serverCtl.readBlob(interaction.url);
if (!contentResp[0]) {
return contentResp;
}
// Store for next steps
this._tempContent = contentResp[2].decodedContent;
return this._createSuccess('Retrieved interaction content');
} catch (err) {
return this._createError(
`Failed to read interaction content: ${err.message}`,
err,
500
);
}
},
// Step 2: Analyze the content
async () => {
// Get word frequencies
const words = this._tempContent
.toLowerCase()
.replace(/[^\w\s]/g, ' ')
.split(/\s+/)
.filter(word => word.length > 3);
const wordFreq = {};
words.forEach(word => {
wordFreq[word] = (wordFreq[word] || 0) + 1;
});
// Sort by frequency
const sortedWords = Object.entries(wordFreq)
.sort(([,a], [,b]) => b - a)
.slice(0, 50)
.reduce((obj, [k, v]) => ({ ...obj, [k]: v }), {});
// Basic sentiment analysis
const positiveWords = ['good', 'great', 'excellent', 'positive', 'advantage', 'benefit'];
const negativeWords = ['bad', 'poor', 'negative', 'disadvantage', 'problem', 'issue'];
let sentimentScore = 0;
words.forEach(word => {
if (positiveWords.includes(word)) sentimentScore++;
if (negativeWords.includes(word)) sentimentScore--;
});
// Create analysis object
const analysis = {
topWords: sortedWords,
totalWords: words.length,
uniqueWords: Object.keys(wordFreq).length,
avgWordLength: words.reduce((sum, word) => sum + word.length, 0) / words.length,
sentiment: {
score: sentimentScore,
normalized: words.length > 0 ? sentimentScore / words.length : 0,
interpretation: sentimentScore > 0 ? 'Positive' :
sentimentScore < 0 ? 'Negative' : 'Neutral'
},
metadata: {
contentType: interaction.content_type,
fileSize: interaction.file_size,
readingTime: interaction.reading_time,
wordCount: interaction.word_count,
pageCount: interaction.page_count
}
};
return this._createSuccess(
`Analysis completed for interaction [${name}]`,
analysis
);
}
], `analyze-interaction-${name}`);
},
this.cacheTimeouts.analysis || 600000,
[
this._cacheKeys.container, // Depends on interaction data
`${this._cacheKeys.byName}_${name}` // Depends on this specific interaction
]
);
} catch (error) {
return this._createError(
`Error analyzing interaction: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
/**
* Extracts topics from an interaction's content
* @param {string} name - Interaction name
* @returns {Promise<Array>} Extracted topics
*/
async extractTopics(name) {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'extractTopics') :
{ end: () => {} };
try {
// Validate parameter
const validationError = this._validateParams(
{ name },
{ name: 'string' }
);
if (validationError) return validationError;
// Use cache for topics with dependency on analysis
const topicsCacheKey = `${this._cacheKeys.topics}_${name}`;
return await this.cache.getOrFetch(
topicsCacheKey,
async () => {
// First get the content analysis which contains word frequencies
const analysisResp = await this.getInteractionAnalysis(name);
if (!analysisResp[0]) {
return analysisResp;
}
const topWords = analysisResp[2].topWords;
// Filter common stop words
const stopWords = ['this', 'that', 'then', 'than', 'they', 'them', 'their', 'there', 'here', 'where'];
const topics = Object.entries(topWords)
.filter(([word]) => !stopWords.includes(word))
.slice(0, 10)
.map(([word, count]) => ({ topic: word, count }));
return this._createSuccess(
`Extracted topics for interaction [${name}]`,
topics
);
},
this.cacheTimeouts.topics || 600000,
[
`${this._cacheKeys.analysis}_${name}` // Depends on content analysis
]
);
} catch (error) {
return this._createError(
`Error extracting topics: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
/**
* Finds similar interactions based on content analysis
* @param {string} name - Name of the base interaction
* @returns {Promise<Array>} Similar interactions
*/
async findSimilar(name) {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'findSimilar') :
{ end: () => {} };
try {
// Validate parameter
const validationError = this._validateParams(
{ name },
{ name: 'string' }
);
if (validationError) return validationError;
// Use cache for similarity results
const similarCacheKey = `${this._cacheKeys.similar}_${name}`;
return await this.cache.getOrFetch(
similarCacheKey,
async () => {
// Get the interaction
const interactionResp = await this.findByName(name);
if (!interactionResp[0]) {
return interactionResp;
}
const interaction = interactionResp[2][0];
// Get all interactions
const allObjectsResp = await this.getAll();
if (!allObjectsResp[0]) {
return allObjectsResp;
}
// Filter out the current interaction
const otherInteractions = allObjectsResp[2].mrJson.filter(i => i.name !== name);
// Compare by metadata similarity
const similarities = otherInteractions.map(other => {
let score = 0;
// Content type match
if (other.content_type === interaction.content_type) score += 1;
// Similar length
const sizeDiff = Math.abs(
(other.file_size || 0) - (interaction.file_size || 0)
) / Math.max(other.file_size || 1, interaction.file_size || 1);
score += (1 - sizeDiff);
// Similar reading time
if (other.reading_time && interaction.reading_time) {
const timeDiff = Math.abs(other.reading_time - interaction.reading_time) /
Math.max(other.reading_time, interaction.reading_time);
score += (1 - timeDiff);
}
// Same company
if (other.linked_companies && interaction.linked_companies) {
const otherCompanies = Object.keys(other.linked_companies);
const thisCompanies = Object.keys(interaction.linked_companies);
for (const company of thisCompanies) {
if (otherCompanies.includes(company)) {
score += 2;
break;
}
}
}
return {
name: other.name,
score,
metadata: {
content_type: other.content_type,
file_size: other.file_size,
reading_time: other.reading_time
}
};
});
// Sort by score and take top 5
const topSimilar = similarities
.sort((a, b) => b.score - a.score)
.slice(0, 5);
return this._createSuccess(
`Found similar interactions for [${name}]`,
topSimilar
);
},
this.cacheTimeouts.similar || 600000,
[
this._cacheKeys.container, // Depends on all interactions
`${this._cacheKeys.byName}_${name}` // Depends on this specific interaction
]
);
} catch (error) {
return this._createError(
`Error finding similar interactions: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
/**
* Group interactions by common attributes
* @param {string} attribute - Attribute to group by (e.g., 'content_type')
* @returns {Promise<Array>} Grouped interactions
*/
async groupBy(attribute) {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'groupBy') :
{ end: () => {} };
try {
// Validate parameter
const validationError = this._validateParams(
{ attribute },
{ attribute: 'string' }
);
if (validationError) return validationError;
// Get all interactions
const allResp = await this.getAll();
if (!allResp[0]) {
return allResp;
}
const interactions = allResp[2].mrJson;
// Group by the specified attribute
const groups = {};
interactions.forEach(interaction => {
const value = interaction[attribute] || 'unknown';
if (!groups[value]) {
groups[value] = [];
}
groups[value].push({
name: interaction.name,
content_type: interaction.content_type,
file_size: interaction.file_size
});
});
// Convert to array of groups
const result = Object.entries(groups).map(([key, items]) => ({
group: key,
count: items.length,
items
}));
return this._createSuccess(
`Interactions grouped by ${attribute}`,
result
);
} catch (error) {
return this._createError(
`Error grouping interactions: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
}