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 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
        Vector taste_vector
        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
        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).
EngagementScore One content's continuous strength [-1,1] + Outcome.
UserSignals THE user model: positives, negatives, tag affinity, taste vector, 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).
Recommendation Final ranked list + strategy ("warm"/"cold") + diagnostics.

Vector = list[float].

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)"]
    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

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.


Full auto-generated reference

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