Duplo

Duplo Cloud #

Prediction model config #

/* API CONFIGURATION */
const workspaceID = "0191a3ed-097f-7dfb-9043-956d54236ce7"; // Replace with your workspace ID
const BASE_API_URL = "/api";

/* Find feature names from https://app.funnelstory.ai/api/internal/prediction/models */

/* EXTRA CONFIGURATION FOR MODEL TRAINING FIELDS */

/*
  additional_traits:
  An array of strings representing additional traits.
  Example: ["AI_advisor", "AI_answering", "pro", "starter"]
*/
const ADDITIONAL_TRAITS = ["cloud"];

/*
  known_positive_factors:
  An array of strings representing factors known to contribute positively.
  Example: ["activity:6_mo_cutoff:Had Online Orders", "activity:6_mo_cutoff:Hit 1K Followers"]
*/
const KNOWN_POSITIVE_FACTORS = [
  "activity:30_days:User Action",
  "activity:total:User Action",
  "activity:30_days:Add Tenant",
  "activity:total:Add Tenant",
  "activity:30_days:Enabled Monitoring",
  "activity:total:Enabled Monitoring",
  "activity:30_days:Enabled Logging",
  "activity:total:Enabled Logging",
  "activity:30_days:Started Onboarding",
  "activity:total:Started Onboarding",
  "property:percentage_cost_increase",
  "property:api_usage_30_days",
  "property:count_of_environments",
  "activity:30_days:Created Account",
  "activity:30_days:Conversation Created",
  "activity:30_days:Completed Implementation",
  "activity:30_days:Infra Created",
  "activity:total:Meeting Occurred",
  "activity:30_days:Meeting Occurred",
  "activity:total:Moved to Support",
  "property:non_duplo_users"
];

/*
  known_negative_factors:
  An array of strings representing factors known to have negative effects.
  Example: ["activity:6_mo_cutoff:Negative Reviews", "trait:open_churn_case=true"]
*/
const KNOWN_NEGATIVE_FACTORS = ["activity:30_days:Created Account", "activity:total:Canceled Project", "activity:30_days:Canceled Project"];

/*
  exclude_factors:
  An array of strings representing factors to exclude from training.
  Example: ["property:last_month_impressions", "activity:6_mo_cutoff:Created User"]
*/
const EXCLUDE_FACTORS = [
  "activity:total:Created User",
  "activity:30_days:Created User",
  "property:node_limit",
  "property:arr_all_time",
  "property:avg_EC2_compute_cost",
  "property:avg_EC2_other_cost",
  "property:avg_ECR_cost",
  "property:avg_ELB_cost",
  "property:avg_IoT_cost",
  "property:avg_KMS_cost",
  "property:avg_RDS_cost",
  "property:avg_RDS_cost",
  "property:avg_SQS_cost",
  "property:avg_VPC_cost",
  "property:avg_cloudwatch_cost",
  "property:avg_elasticache_cost",
  "property:avg_inspector_cost",
  "property:avg_route_53_cost",
  "property:avg_secrets_manager_cost",
];

/* 
  Create safe fallbacks so that if any of the above are missing or not arrays,
  they default to an empty array.
*/
const safeAdditionalTraits = Array.isArray(ADDITIONAL_TRAITS)
  ? ADDITIONAL_TRAITS
  : [];
const safeKnownPositiveFactors = Array.isArray(KNOWN_POSITIVE_FACTORS)
  ? KNOWN_POSITIVE_FACTORS
  : [];
const safeKnownNegativeFactors = Array.isArray(KNOWN_NEGATIVE_FACTORS)
  ? KNOWN_NEGATIVE_FACTORS
  : [];
const safeExcludeFactors = Array.isArray(EXCLUDE_FACTORS)
  ? EXCLUDE_FACTORS
  : [];

/* MAIN SCRIPT */
const trainWithAudiences = async (type) => {
  // Fetch audiences (including hidden ones)
  const response = await fetch(
    `${BASE_API_URL}/audiences?include_hidden=true`,
    {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
        "fs-workspace-id": workspaceID,
      },
    }
  );
  const audiencesData = await response.json();

  // Helper: Combine considered IDs from Churn and Retention audiences
  const getCombinedConsideredIds = (audiences, type) => {
    const churnAudience = audiences.find((aud) => aud.name === "Churn");
    const retentionAudience = audiences.find((aud) => aud.name === "Retention");

    if (type === "user") {
      const churnUserIds = churnAudience
        ? churnAudience.data.matching_fs_user_ids
        : [];
      const retentionUserIds = retentionAudience
        ? retentionAudience.data.matching_fs_user_ids
        : [];
      return Array.from(new Set([...churnUserIds, ...retentionUserIds]));
    } else {
      const churnAccountIds = churnAudience
        ? churnAudience.data.matching_account_ids
        : [];
      const retentionAccountIds = retentionAudience
        ? retentionAudience.data.matching_account_ids
        : [];
      return Array.from(new Set([...churnAccountIds, ...retentionAccountIds]));
    }
  };

  // For a given audience/model, build the training request body and execute the POST call
  const trainModel = async (aud, model, consideredIds) => {
    const url =
      type === "user"
        ? `${BASE_API_URL}/internal/prediction/models/user_${model}/train`
        : `${BASE_API_URL}/internal/prediction/models/${model}/train`;

    const targetIds =
      type === "user"
        ? { target_user_fsids: aud.data.matching_fs_user_ids }
        : { target_account_ids: aud.data.matching_account_ids };

    const consideredKey =
      type === "user" ? "considered_user_fsids" : "considered_account_ids";

    // Training parameters
    const params = {
      num_trees: 25,
      tree_depth: 100,
      learning_rate: 0.1,
      iterations: 100,
      lambda: 0.01,
      regularizer: "ridge",
      ensemble_weights: { lr: 0.3, rf: 0.7 },
    };

    // Build the POST body including the extra configuration fields
    const body = {
      ...targetIds,
      [consideredKey]: consideredIds,
      params,
      additional_traits: safeAdditionalTraits,
      known_positive_factors: safeKnownPositiveFactors,
      known_negative_factors: safeKnownNegativeFactors,
      exclude_factors: safeExcludeFactors,
      // Only ignore target IDs when training the churn model
      //ignore_target_ids: model === "churn",
    };

    await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "fs-workspace-id": workspaceID,
      },
      body: JSON.stringify(body),
    });
  };

  // Get audiences array (adjust based on your API response shape)
  const audiencesArray = audiencesData.response.audiences;
  const consideredIds = getCombinedConsideredIds(audiencesArray, type);

  // Filter to include only hidden 'Churn' and 'Retention' audiences
  const trainingAudiences = audiencesArray.filter(
    (aud) => aud.hidden && (aud.name === "Churn" || aud.name === "Retention")
  );

  // Start training concurrently for each selected audience
  const trainPromises = trainingAudiences.map((aud) => {
    const model = aud.name.toLowerCase(); // Either "churn" or "retention"
    return trainModel(aud, model, consideredIds);
  });
  await Promise.all(trainPromises);

  // Update the scores after training
  const updateUrl =
    type === "user"
      ? `${BASE_API_URL}/internal/prediction/scores/users/update`
      : `${BASE_API_URL}/internal/prediction/scores/accounts/update`;

  await fetch(updateUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "fs-workspace-id": workspaceID,
    },
  });
};