Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
# AI Sorting

Intelligent content ordering for Drupal Views using machine learning. Content
automatically learns which items engage users most and surfaces the
best-performing content.
Intelligent ordering for Drupal Views using machine learning. Automatically learns which items engage users most and surfaces the best-performing content, regardless of entity type.

## Features

- Automatic content optimization based on user engagement
- Thompson Sampling machine learning algorithm
- Views integration as sort plugin
- Automatic cache management
- Real-time learning and adaptation
- **Universal entity support** - Works with nodes, users, taxonomy terms, media, custom entities, and external data
- **Thompson Sampling algorithm** - Advanced machine learning for exploration vs exploitation
- **Cold start handling** - New items get proper exploration scores automatically
- **Views integration** - Simple sort plugin that works with any Views-compatible data source
- **Real-time learning** - Continuous adaptation based on user interactions
- **Fail-hard debugging** - No silent fallbacks, issues are immediately visible

## Setup

1. Install the module (requires [RL module](https://www.drupal.org/project/rl))
2. Edit any View display
2. Edit any View display (nodes, users, terms, media, custom entities)
3. Add "AI Sorting" as a sort criteria
4. Configure cache refresh rate (30 seconds to 10 minutes)
5. Save - content immediately begins learning from user interactions
4. Configure cache refresh rate and time window options
5. Save - items immediately begin learning from user interactions

## How It Works

1. **Track Engagement** - JavaScript monitors when content is viewed and clicked
2. **Learn Patterns** - Machine learning identifies high-performing content
3. **Optimize Order** - Best content automatically moves to prominent positions
4. **Continuous Improvement** - Performance gets better with every visitor
1. **Track Engagement** - JavaScript monitors when items are viewed and clicked
2. **Learn Patterns** - Thompson Sampling identifies high-performing items
3. **Optimize Order** - Best items automatically move to prominent positions
4. **Handle New Items** - New content gets exploration scores for fair exposure
5. **Continuous Improvement** - Performance gets better with every visitor

## Use Cases

Expand Down
12 changes: 2 additions & 10 deletions ai_sorting.libraries.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
ai_sorting_turns:
ai_sorting_tracking:
version: VERSION
js:
js/ai-sorting-turns.js: {}
dependencies:
- core/drupal
- core/drupalSettings

ai_sorting_rewards:
version: VERSION
js:
js/ai-sorting-rewards.js: {}
js/ai-sorting-tracking.js: {}
dependencies:
- core/drupal
- core/drupalSettings
112 changes: 77 additions & 35 deletions ai_sorting.module
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
* Primary module file for AI Sorting.
*/

use Drupal\node\NodeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\views\ViewExecutable;
use Drupal\Core\Url;

/**
* Implements hook_help().
Expand Down Expand Up @@ -36,56 +35,82 @@ function ai_sorting_views_pre_render(ViewExecutable $view) {
return;
}

// Collect node IDs and URLs for JavaScript tracking.
$nids = [];
$nid_url_map = [];
// Get the base field for this view's data source.
$base_field = $view->storage->get('base_field');
if (empty($base_field)) {
// Can't track without a unique identifier.
return;
}

foreach ($view->result as $index => $row) {
// Collect entity IDs and URLs for JavaScript tracking.
$entity_ids = [];
$entity_url_map = [];

$nid = NULL;
foreach ($view->result as $index => $row) {
$entity_id = NULL;
$entity = NULL;

// Try different ways to get the node ID.
if (isset($row->nid)) {
$nid = $row->nid;
// Try to get the entity ID from the base field.
if (isset($row->$base_field)) {
$entity_id = $row->$base_field;
}
elseif (isset($row->node_field_data_nid)) {
$nid = $row->node_field_data_nid;
}
elseif (isset($row->_entity) && $row->_entity instanceof NodeInterface) {
$nid = $row->_entity->id();

// Try to get the entity object.
if (isset($row->_entity) && $row->_entity instanceof EntityInterface) {
$entity = $row->_entity;
if (!$entity_id) {
$entity_id = $entity->id();
}
}
elseif (isset($row->_object) && method_exists($row->_object, 'id')) {
$nid = $row->_object->id();
elseif (isset($row->_object) && $row->_object instanceof EntityInterface) {
$entity = $row->_object;
if (!$entity_id) {
$entity_id = $entity->id();
}
}

if ($nid) {
$nids[] = $nid;
$nid_url_map[$nid] = Url::fromRoute('entity.node.canonical',
['node' => $nid])->toString();
if ($entity_id) {
$entity_ids[] = (string) $entity_id;

// Generate URL for the entity if we have the entity object.
$url = '';
if ($entity && $entity->hasLinkTemplate('canonical')) {
try {
$url = $entity->toUrl('canonical')->toString();
}
catch (\Exception $e) {
// If URL generation fails, leave empty.
$url = '';
}
}

$entity_url_map[(string) $entity_id] = $url;
}
}

if (!empty($nids)) {
if (!empty($entity_ids)) {
// Generate experiment UUID from view and display.
$experiment_uuid = sha1($view->id() . ':' . $view->current_display);

// Create human-readable experiment name.
$experiment_name = $view->id() . ':' . $view->current_display;

// Register this experiment with the RL module.
$registration_service = \Drupal::service(
'ai_sorting.experiment_registration'
);
$registration_service->registerExperiment($experiment_uuid);
$registration_service->registerExperiment($experiment_uuid, $experiment_name);

// Get RL module path for optimized endpoint.
$rl_path = \Drupal::service('extension.list.module')->getPath('rl');
$base_path = \Drupal::request()->getBasePath();

// Attach JavaScript libraries and settings.
$view->element['#attached']['library'][] = 'ai_sorting/ai_sorting_turns';
$view->element['#attached']['library'][] = 'ai_sorting/ai_sorting_rewards';
// Attach JavaScript library and settings.
$view->element['#attached']['library'][] = 'ai_sorting/ai_sorting_tracking';
$view->element['#attached']['drupalSettings']['aiSorting']['views'][$view->id()] = [
'experimentUuid' => $experiment_uuid,
'nids' => $nids,
'nidUrlMap' => $nid_url_map,
'entityIds' => $entity_ids,
'entityUrlMap' => $entity_url_map,
'rlEndpointUrl' => "{$base_path}/{$rl_path}/rl.php",
'displayId' => $view->current_display,
];
Expand All @@ -96,11 +121,28 @@ function ai_sorting_views_pre_render(ViewExecutable $view) {
* Implements hook_views_data_alter().
*/
function ai_sorting_views_data_alter(array &$data) {
$data['node']['ai_sorting'] = [
'title' => t('AI Sorting'),
'help' => t('Sort content using reinforcement learning algorithm.'),
'sort' => [
'id' => 'ai_sorting',
],
];
// Add AI Sorting to all entity base tables.
$entity_type_manager = \Drupal::entityTypeManager();
$entity_types = $entity_type_manager->getDefinitions();

foreach ($entity_types as $entity_type_id => $entity_type) {
// Only add to entity types that have a data table (appear in Views).
$data_table = $entity_type->getDataTable();
$base_table = $entity_type->getBaseTable();

// Use data table if available, otherwise base table.
$table_name = $data_table ?: $base_table;

if ($table_name && isset($data[$table_name])) {
$data[$table_name]['ai_sorting'] = [
'title' => t('AI Sorting'),
'help' => t('Sort @entity_type using reinforcement learning algorithm.', [
'@entity_type' => $entity_type->getLabel(),
]),
'sort' => [
'id' => 'ai_sorting',
],
];
}
}
}
3 changes: 0 additions & 3 deletions ai_sorting.permissions.yml

This file was deleted.

13 changes: 0 additions & 13 deletions ai_sorting.services.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
services:
ai_sorting.experiment_resolver:
class: Drupal\ai_sorting\Service\ExperimentResolver

ai_sorting.total_trials_service:
class: Drupal\ai_sorting\Service\TotalTrialsService
arguments: ['@rl.experiment_manager']

ai_sorting.experiment_decorator:
class: Drupal\ai_sorting\Decorator\AiSortingExperimentDecorator
arguments: ['@entity_type.manager']
tags:
- { name: rl_experiment_decorator }

ai_sorting.experiment_registration:
class: Drupal\ai_sorting\Service\ExperimentRegistrationService
arguments: ['@rl.experiment_registry']
98 changes: 0 additions & 98 deletions docs/ai_sorting_project_desc.html

This file was deleted.

47 changes: 0 additions & 47 deletions js/ai-sorting-rewards.js

This file was deleted.

Loading
Loading