Skip to content

Contracts & Models

recsys/contracts/ is the pure typed core, no IO imports. Everything downstream speaks these models, enums, config, and Protocols. Full auto-generated signatures live in the Code reference; this page is the conceptual map.

Domain models (models.py)

classDiagram
    class Content {
        str id
        ContentType content_type
        str title
        str text
        list~Tag~ tags
        int word_count
        bool has_image
        float lat
        float lon
    }
    class Tag {
        str facet
        str label
        float weight
        +key() str
    }
    class InteractionEvent {
        str user_id
        str event
        datetime ts
        str request_id
        str content_id
        float dwell_seconds
        EndReason end_reason
        str query_text
        list~str~ impressions
        dict survey_answers
    }
    class EngagementScore {
        str content_id
        Outcome outcome
        float strength
        datetime ts
    }
    class UserSignals {
        str user_id
        dict positives
        dict negatives
        dict tag_affinity
        dict tag_aversion
        Vector taste_vector
        Vector recency_vector
        dict behavior
        dict demographics
        +is_cold() bool
    }
    class Candidate {
        str content_id
        str generated_by
        float base_score
    }
    class ScoredCandidate {
        str content_id
        float final_score
        dict breakdown
        list~float~ features
        Content content
    }
    class Recommendation {
        str user_id
        list~ScoredCandidate~ items
        str strategy
        dict diagnostics
    }
    Content "1" o-- "*" Tag
    InteractionEvent ..> EngagementScore : scored into
    EngagementScore ..> UserSignals : folded into
    UserSignals ..> Candidate : generates
    Candidate ..> ScoredCandidate : scored into
    ScoredCandidate ..> Recommendation : ranked into
Model Role
Content Normalized item: structure, tags, embedding-side metadata, optional geo.
Tag Expert tag facet:label with weight; .key -> "facet:label".
InteractionEvent Canonical event from any source (RudderStack / PostHog / Postgres). Carries an optional request_id that echoes a rec response, joining a later reward to the served impression.
EngagementScore One content's continuous strength [-1,1] + Outcome.
UserSignals THE user model: positives, negatives, tag affinity + tag_aversion (disliked-theme penalties), taste_vector and recency_vector, a behavior engagement summary (drives explanations), demographics. is_cold when no positives.
Candidate A recall result tagged with generated_by ("semantic"/"tag"/"geo").
ScoredCandidate Candidate + final_score + per-scorer breakdown (explainability) + ordered features (the context vector logged for bandit training).
Recommendation Final ranked list + strategy ("warm"/"cold") + diagnostics.

Vector = list[float].

Explainability contracts

A glass-box reading of the user model, grounded in museum-visitor theory. Pure, derived from UserSignals + the content taxonomy. See Explainability.

Model Role
Interest One taxonomy tag (facet/label) with affinity weight and evidence (the content ids that drove it). Reused for aversions.
VisitorType Falk (2009) visit identity: type (Explorer / Hobbyist / Recharger / Experience-Seeker / Facilitator) + confidence (margin to runner-up) + rationale + per-type scores.
PersonaExplanation The full persona: interests / aversions, engagement_style, experience_preference (Pekarik), visitor_type (Falk), trajectory, demographics, and an optional verbalized summary.

Enums (enums.py)

Enum Values
ContentType text_item, image_item, video_item, audio_item, exhibition, tag, poi
EndReason next_button (engaged) · link (mild) · close_button (weak) · abandon (negative)
Outcome positive, negative, neutral

Config (config.py)

All tunables in one typed place (RecConfig, Pydantic), grouped:

EngagementWeights: dwell=0.4  completion=0.3  revisit=0.2  survey=0.1
FusionWeights:     semantic=0.5  tag=0.3  geo=0.0  popularity=0.0
RecConfig:         reading_speed_wps=4.2  img_extra_time=1.3  dwell_cap_ratio=2.0
                   positive_threshold=0.30  negative_threshold=-0.05
                   half_life_days=14.0  soft_negative_weight=0.30
                   pool_per_generator=30  final_limit=10
                   mmr_lambda=0.7  cold_start_min_positives=1

Ports (ports.py), the only injection seams

flowchart LR
    subgraph protocols["Protocols (runtime_checkable)"]
        es["EventSource<br/>fetch_events(user_id)"]
        cstore["ContentStore<br/>get · get_vectors<br/>search_vector · search_tags"]
        ums["UserModelStore<br/>get_signals · save_signals"]
        em["EmbeddingModel<br/>dim · encode(text)"]
        imp["ImpressionStore<br/>put · get · consume"]
    end
    subgraph real["Real adapters"]
        rb["RedisEventBuffer"]
        qcs["QdrantContentStore"]
        rms["RedisUserModelStore"]
        fe["FastEmbedModel"]
    end
    subgraph fake["Test fakes"]
        fes["FakeEventSource"]
        fcs["FakeContentStore"]
        imu["InMemoryUserModelStore"]
        ime["InMemoryEmbeddingModel"]
    end
    es -.-> rb & fes
    cstore -.-> qcs & fcs
    ums -.-> rms & imu
    em -.-> fe & ime
    imp -.-> ris["RedisImpressionStore"] & imi["InMemoryImpressionStore"]

A Protocol exists only where there are ≥2 implementations or a deterministic fake is needed. Pure functions (engagement, scorers, fusion) take typed models directly, no Protocol wrapper.

The ImpressionStore port keys served feature vectors by request_id (put / get / consume) so a later reward event can drive an online bandit update, then consume drops the impression for idempotency. ContentStore also carries sample, search_filter, and search_geo (the geo radius filter, independent of the tag filter). A DemographicsProvider port supplies survey demographics for the cold-start tag bridge. See Adapters and Bandit / online learning.


Full auto-generated reference

See Code reference -> Recsys package for every field, default, and validator rendered from source.