Skip to content

PersonalizedDJ: Agentic AI for Music

The PersonalizedDJ is QFZZ's intelligent recommendation engine that learns your musical preferences and creates tailored playlists.

Overview

Unlike traditional recommendation systems that rely solely on collaborative filtering, QFZZ's PersonalizedDJ combines multiple approaches:

  • Content-based filtering: Analyzes track attributes
  • Collaborative signals: Learns from user behavior patterns
  • Agentic AI: Autonomous decision-making for discovery
  • Real-time adaptation: Responds immediately to feedback

Key Features

🎯 Multi-Dimensional Taste Profiling

The DJ builds a comprehensive profile of your musical taste:

class UserProfile:
    genres: Dict[str, float]          # Genre preferences with weights
    artists: Dict[str, float]         # Artist preferences
    moods: Dict[str, float]          # Mood preferences
    energy_level: float              # Preferred energy level (0.0-1.0)
    tempo_preference: str            # slow, medium, fast, or varied
    discovery_factor: float          # Exploration vs. exploitation (0.0-1.0)
    interactions: List[Dict]         # Full interaction history

🔍 Intelligent Discovery

Balance familiarity with exploration:

station.add_listener(
    user_id="alice",
    preferences={
        'genres': {'rock': 0.8, 'indie': 0.6},
        'discovery_factor': 0.2  # 20% new/unexpected tracks
    }
)

Discovery Factor Guide: - 0.0: Only familiar content (no exploration) - 0.2: Mostly familiar with some discovery (recommended) - 0.5: Equal balance - 0.8: Mostly discovery with some familiar - 1.0: Complete exploration (random)

🎨 Mood and Energy Matching

Fine-tune recommendations by mood and energy:

preferences = {
    'energy_level': 0.7,  # 0.0 = calm, 1.0 = energetic
    'moods': {
        'upbeat': 0.8,
        'mellow': 0.3
    },
    'tempo_preference': 'fast'  # slow, medium, fast, varied
}

🧠 Learning from Feedback

The DJ learns from both implicit and explicit signals:

Implicit Feedback:

# User plays track (positive signal: +0.05)
station.record_interaction(user_id, track_id, "play")

# User skips track (negative signal: -0.05)
station.record_interaction(user_id, track_id, "skip")

Explicit Feedback:

# User likes track (positive signal: +0.1)
station.record_interaction(user_id, track_id, "like")

# User favorites track (strong positive: +0.2)
station.record_interaction(user_id, track_id, "favorite")

# User rates track (scaled: -0.1 to +0.1)
station.record_interaction(user_id, track_id, "rate", rating=0.8)

How It Works

1. Profile Creation

When a user first connects:

from qfzz.dj import PersonalizedDJ

dj = PersonalizedDJ()

# Create profile with initial preferences
profile = dj.get_or_create_profile(
    user_id="bob",
    initial_preferences={
        'genres': {'jazz': 0.9, 'blues': 0.7},
        'artists': {'Miles Davis': 0.9},
        'energy_level': 0.4,
        'discovery_factor': 0.15
    }
)

2. Track Scoring Algorithm

Each track is scored based on multiple factors:

def calculate_track_score(track, profile):
    score = 0.0

    # Genre matching (30% weight)
    if track['genre'] in profile.genres:
        score += profile.genres[track['genre']] * 0.3
    elif track['genre'] in similar_genres(profile.genres):
        score += 0.5 * 0.3  # Partial match for similar genres

    # Artist matching (25% weight)
    if track['artist'] in profile.artists:
        score += profile.artists[track['artist']] * 0.25

    # Energy level matching (20% weight)
    energy_diff = abs(track['energy'] - profile.energy_level)
    score += (1.0 - energy_diff) * 0.2

    # Tempo matching (15% weight)
    if track['tempo'] == profile.tempo_preference or profile.tempo_preference == 'varied':
        score += 0.15

    # Mood matching (10% weight)
    if track['mood'] in profile.moods:
        score += profile.moods[track['mood']] * 0.1

    return score

Scoring Breakdown:

Factor Weight Description
Genre 30% Direct genre match or similarity
Artist 25% Known artist preference
Energy 20% Energy level compatibility
Tempo 15% Tempo preference match
Mood 10% Mood compatibility

3. Discovery Application

After scoring, apply discovery factor:

def apply_discovery(scored_tracks, discovery_factor):
    # Split into familiar and discovery pools
    split_point = int(len(scored_tracks) * (1.0 - discovery_factor))

    # Take high-scoring tracks
    familiar = scored_tracks[:split_point]

    # Add random discovery tracks
    discovery_pool = scored_tracks[split_point:]
    num_discovery = int(len(familiar) * discovery_factor / (1.0 - discovery_factor))
    discovery = random.sample(discovery_pool, min(num_discovery, len(discovery_pool)))

    return [track for _, track in familiar] + [track for _, track in discovery]

4. Continuous Learning

Preferences update incrementally with each interaction:

def update_from_feedback(profile, track, interaction_type, rating):
    # Calculate feedback strength
    strength = FEEDBACK_STRENGTH[interaction_type]

    # Update genre preference
    if 'genre' in track:
        current = profile.genres.get(track['genre'], 0.5)
        new_weight = clamp(current + strength, 0.0, 1.0)
        profile.genres[track['genre']] = new_weight

    # Similarly for artists, moods, etc.

Genre Similarity Mapping

The DJ understands genre relationships:

genre_similarity = {
    'rock': ['alternative', 'indie', 'punk', 'metal'],
    'pop': ['dance', 'electronic', 'indie-pop', 'synth-pop'],
    'jazz': ['blues', 'soul', 'funk', 'swing'],
    'classical': ['orchestral', 'baroque', 'romantic', 'contemporary'],
    'hip-hop': ['rap', 'trap', 'r&b', 'urban'],
    'electronic': ['techno', 'house', 'trance', 'ambient'],
    'folk': ['acoustic', 'country', 'americana', 'singer-songwriter'],
    'metal': ['hard-rock', 'progressive', 'death-metal', 'black-metal']
}

This enables: - Discovery of related genres - Expansion of musical horizons - Natural progression in tastes

Advanced Usage

Custom Content Catalog

Add your own music catalog:

from qfzz.dj import PersonalizedDJ

dj = PersonalizedDJ()

# Add custom tracks
my_tracks = [
    {
        'track_id': 'custom_001',
        'title': 'Moonlight Sonata',
        'artist': 'Beethoven',
        'genre': 'classical',
        'mood': 'contemplative',
        'energy': 0.3,
        'tempo': 'slow',
        'duration': 900,
        'content_id': 'beethoven_moonlight',
        'creator_id': 'beethoven'
    },
    # ... more tracks
]

dj.add_content(my_tracks)

Access User Profile

Inspect and analyze user preferences:

# Get profile
profile = dj.get_profile("user_id")

if profile:
    print(f"Top genres: {sorted(profile.genres.items(), key=lambda x: x[1], reverse=True)[:3]}")
    print(f"Top artists: {sorted(profile.artists.items(), key=lambda x: x[1], reverse=True)[:3]}")
    print(f"Energy level: {profile.energy_level}")
    print(f"Discovery factor: {profile.discovery_factor}")
    print(f"Total interactions: {len(profile.interactions)}")

Adjust Discovery Over Time

Dynamically adjust discovery based on user maturity:

# New user: Higher discovery
if len(profile.interactions) < 50:
    profile.discovery_factor = 0.3
# Established user: Lower discovery
elif len(profile.interactions) > 500:
    profile.discovery_factor = 0.1
# Regular user: Balanced
else:
    profile.discovery_factor = 0.2

Context-Aware Recommendations

Adapt recommendations to context:

import datetime

def get_time_of_day_preferences():
    hour = datetime.datetime.now().hour

    if 6 <= hour < 12:  # Morning
        return {'energy_level': 0.6, 'moods': {'upbeat': 0.8}}
    elif 12 <= hour < 18:  # Afternoon
        return {'energy_level': 0.7, 'moods': {'energetic': 0.7}}
    elif 18 <= hour < 22:  # Evening
        return {'energy_level': 0.5, 'moods': {'mellow': 0.7}}
    else:  # Night
        return {'energy_level': 0.3, 'moods': {'calm': 0.8}}

# Use context in recommendations
context_prefs = get_time_of_day_preferences()
playlist = station.generate_playlist(user_id)

Best Practices

1. Provide Initial Preferences

Help the DJ bootstrap:

# Good: Provide initial preferences
station.add_listener(
    user_id="user",
    preferences={
        'genres': {'rock': 0.7, 'indie': 0.6},
        'artists': {'Radiohead': 0.9, 'Arcade Fire': 0.8}
    }
)

# Suboptimal: Empty preferences (cold start)
station.add_listener(user_id="user")

2. Balance Discovery Factor

Too low = echo chamber, too high = chaos:

# Too low: Repetitive
preferences = {'discovery_factor': 0.05}

# Recommended: Balanced
preferences = {'discovery_factor': 0.2}

# Too high: Random
preferences = {'discovery_factor': 0.9}

3. Provide Rich Feedback

More feedback = better recommendations:

# Good: Rich feedback
station.record_interaction(user_id, track_id, "play")
station.record_interaction(user_id, track_id, "like")
station.record_interaction(user_id, track_id, "favorite")

# Better: With ratings
station.record_interaction(user_id, track_id, "rate", rating=0.85)

4. Regular Profile Analysis

Monitor profile evolution:

def analyze_profile(profile):
    """Analyze user profile for insights."""

    # Genre diversity
    genre_count = len(profile.genres)
    top_genre_weight = max(profile.genres.values()) if profile.genres else 0

    # Artist diversity
    artist_count = len(profile.artists)

    # Interaction patterns
    interaction_count = len(profile.interactions)
    recent_interactions = profile.interactions[-20:] if len(profile.interactions) >= 20 else profile.interactions

    likes = sum(1 for i in recent_interactions if i['type'] == 'like')
    skips = sum(1 for i in recent_interactions if i['type'] == 'skip')

    return {
        'genre_diversity': genre_count,
        'genre_concentration': top_genre_weight,
        'artist_diversity': artist_count,
        'total_interactions': interaction_count,
        'recent_like_rate': likes / len(recent_interactions) if recent_interactions else 0,
        'recent_skip_rate': skips / len(recent_interactions) if recent_interactions else 0
    }

Performance Optimization

Caching

Profiles are cached in memory: - O(1) lookup time - No disk I/O for hot profiles - Incremental updates

Efficient Scoring

Track scoring is vectorized where possible: - Pre-compute genre similarities - Batch profile lookups - Lazy track loading

Memory Management

Profiles persist in memory but can be persisted:

def save_profile(profile, path):
    """Save profile to disk."""
    import pickle
    with open(path, 'wb') as f:
        pickle.dump(profile, f)

def load_profile(path):
    """Load profile from disk."""
    import pickle
    with open(path, 'rb') as f:
        return pickle.load(f)

Future Enhancements

See Roadmap for planned features:

  • [ ] Deep learning recommendation models
  • [ ] Audio feature extraction and embedding
  • [ ] Context-aware recommendations (time, location, activity)
  • [ ] Reinforcement learning for long-term satisfaction
  • [ ] Multi-armed bandit algorithms
  • [ ] Active learning for efficient preference elicitation
  • [ ] Cross-user collaborative filtering
  • [ ] Session-based recommendation

Research Background

The PersonalizedDJ incorporates techniques from:

  • Music Information Retrieval (MIR): Genre classification, similarity
  • Recommendation Systems: Collaborative and content-based filtering
  • Multi-Armed Bandits: Exploration-exploitation trade-off
  • Reinforcement Learning: Long-term reward optimization

For detailed research analysis, see Research Section.


Next: Blockchain Trust → | API Reference →