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.