ExamplesRecommendation Memory

Recommendation Memory

For LLM prompt context via semantic search over ledger events, see Vector Memory and the Support Copilot example. This page covers deterministic entity-state memory built in core rules.

Causet’s event-sourced entity state is a natural foundation for personalization features. This example shows how to build rich user memory from concert events — the kind of structured context that powers recommendations, AI features, and “Year in Review” summaries.


The idea

Instead of maintaining separate analytics tables, user preferences, and recommendation signals, everything derives from the event stream:

  • A user checks into 48 concerts — concert_count: 48
  • They attend Pearl Jam 7 times — tracked in top_artists
  • They travel 1,200 miles to shows — total_distance_miles
  • Their favorite venue emerges from check-in frequency — queryable from projection

This data is:

  • Replayable — rebuild it at any time from the ledger
  • Auditable — every number traces back to a specific event
  • Consistent — derived from the same event stream as everything else
  • AI-ready — structured context for LLM features

State definition

# states/user.state.causet
state:
  user:
    entity_key: user_id
    fields:
      - name: concert_count
        type: int
        default: 0
      - name: unique_artists_seen
        type: int
        default: 0
      - name: unique_venues_visited
        type: int
        default: 0
      - name: top_artists
        type: array
        item_type: object
        item_fields:
          artist_id:  { type: string, required: true }
          play_count: { type: int,    required: true }
        default: []
      - name: genre_counts
        type: array
        item_type: object
        item_fields:
          genre:      { type: string, required: true }
          count:      { type: int,    required: true }
        default: []

Building memory via core rules

When a user checks in at a show, core rules update the memory fields:

# actions/checkin.actions.causet
actions:
  CHECK_IN:
    state: user
    input:
      show_id:   { type: string, required: true }
      user_id:   { type: string, required: true }
      artist_id: { type: string, required: true }
      genre:     { type: string, required: true }
      venue_id:  { type: string, required: true }
    core:
      rules:
        - name: increment_concert_count
          when: {}
          then:
            - op: add
              path: /concert_count
              value: 1
 
        - name: update_artist_memory
          when: {}
          then:
            - op: find
              path: /top_artists
              where: "it.artist_id == intent.artist_id"
              as: existing_artist
            - op: if
              expr: "existing_artist == null"
              then:
                - op: push
                  path: /top_artists
                  value:
                    artist_id:  intent.artist_id
                    play_count: 1
              else:
                - op: map
                  path: /top_artists
                  as: a
                  value: "a.artist_id == intent.artist_id ? {artist_id: a.artist_id, play_count: a.play_count + 1} : a"

Memory projection for queries

While entity state holds the memory, a projection makes it queryable at scale:

# projections/user_memory.projections.causet
projections:
  user_concert_summary:
    source_events: [USER_CHECKED_IN]
    target:
      table: user_concert_summary
      primary_key: [user_id]
    fields:
      user_id:                TEXT
      concert_count:          BIGINT
      unique_artists_count:   BIGINT
      unique_venues_count:    BIGINT
      last_checkin_at:        BIGINT
    derive:
      user_id:        event.user_id
      last_checkin_at: event.ts
    aggregates:
      USER_CHECKED_IN:
        concert_count: { op: add, by: 1, floor: 0 }
    indexes:
      - columns: [concert_count]
        direction: desc
 
  artist_affinity:
    source_events: [USER_CHECKED_IN, ARTIST_FOLLOWED]
    target:
      table: artist_affinity
      primary_key: [user_id, artist_id]
    fields:
      user_id:       TEXT
      artist_id:     TEXT
      checkin_count: BIGINT
      follow_count:  BIGINT
      affinity_score: BIGINT
      updated_at:    BIGINT
    derive:
      user_id:    event.user_id
      artist_id:  event.artist_id
      updated_at: event.ts
    aggregates:
      USER_CHECKED_IN:
        checkin_count:  { op: add, by: 3, floor: 0 }   # checkin = 3x weight
        affinity_score: { op: add, by: 3, floor: 0 }
      ARTIST_FOLLOWED:
        follow_count:   { op: add, by: 1, floor: 0 }
        affinity_score: { op: add, by: 1, floor: 0 }
    indexes:
      - columns: [user_id, affinity_score]
        direction: desc

Queries for recommendations

# queries/recommendations.queries.causet
queries:
  top_artists_for_user:
    from: artist_affinity
    input:
      user_id: { type: string, required: true }
    where:
      user_id: { eq: input.user_id }
    order_by:
      affinity_score: desc
    limit: 10
 
  top_concert_goers:
    from: user_concert_summary
    fields:
      - user_concert_summary.user_id
      - user_concert_summary.concert_count
      - user_concert_summary.last_checkin_at
    order_by:
      concert_count: desc
    limit: 100

Why this matters for AI

When you have a grounded, replayable event history, AI features get reliable context:

User memory for user-1:
  concert_count: 48
  top_artists: [pearl-jam (×7), radiohead (×5), arcade-fire (×4)]
  checkin_venues: [barclays-center (×12), stone-pony (×8)]
  affinity_score: pearl-jam=24, radiohead=17, arcade-fire=14

This is not a summary generated once and forgotten. It’s derived from immutable events and can be rebuilt, audited, or corrected at any time. An AI recommendation engine reading this context can trust it.