Source: gitHubServer/entities/interactions.js

/**
 * 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();
    }
  }
}