haystack-ml-stack 0.4.8__tar.gz → 0.4.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/PKG-INFO +1 -1
  2. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/README.md +0 -0
  3. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/pyproject.toml +1 -1
  4. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/__init__.py +0 -0
  5. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/_kafka.py +2 -1
  6. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/_serializers.py +3 -0
  7. haystack_ml_stack-0.4.10/src/haystack_ml_stack/_version.py +1 -0
  8. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/app.py +12 -1
  9. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/cache.py +0 -0
  10. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/dynamo.py +14 -12
  11. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/exceptions.py +0 -0
  12. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/generated/__init__.py +0 -0
  13. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/generated/v1/__init__.py +0 -0
  14. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/generated/v1/features_pb2.py +34 -34
  15. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/generated/v1/features_pb2.pyi +8 -2
  16. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/model_store.py +0 -0
  17. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/settings.py +0 -0
  18. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack/utils.py +2 -0
  19. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack.egg-info/PKG-INFO +1 -1
  20. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack.egg-info/SOURCES.txt +0 -0
  21. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack.egg-info/dependency_links.txt +0 -0
  22. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack.egg-info/requires.txt +0 -0
  23. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/src/haystack_ml_stack.egg-info/top_level.txt +0 -0
  24. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/tests/test_serializers.py +61 -0
  25. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/tests/test_utils.py +0 -0
  26. haystack_ml_stack-0.4.8/src/haystack_ml_stack/_version.py +0 -1
  27. {haystack_ml_stack-0.4.8 → haystack_ml_stack-0.4.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haystack-ml-stack
3
- Version: 0.4.8
3
+ Version: 0.4.10
4
4
  Summary: Functions related to Haystack ML
5
5
  Author-email: Oscar Vega <oscar@haystack.tv>
6
6
  License: MIT
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "haystack-ml-stack"
8
- version = "0.4.8"
8
+ version = "0.4.10"
9
9
  description = "Functions related to Haystack ML"
10
10
  readme = "README.md"
11
11
  authors = [{ name = "Oscar Vega", email = "oscar@haystack.tv" }]
@@ -72,7 +72,7 @@ async def send_channels_to_kafka(
72
72
  "userid": user.get("userid"),
73
73
  "client_os": user.get("clientOs"),
74
74
  "model_input": {"user": user, "channels": channels},
75
- "model_output": model_output,
75
+ "model_output": {c: v for c, v in model_output.items() if c != "__meta__"},
76
76
  "model_name": state["model_name"].replace(".pkl", "")
77
77
  if state["model_name"]
78
78
  else None,
@@ -83,6 +83,7 @@ async def send_channels_to_kafka(
83
83
  "user_features": state.get("user_features", []),
84
84
  "global_features": state.get("global_features", []),
85
85
  "query_params": query_params,
86
+ **model_output.get("__meta__", {})
86
87
  },
87
88
  "processed_at": processed_at.isoformat(),
88
89
  }
@@ -275,6 +275,8 @@ class UserPlaylistStatsSerializerV1(SimpleSerializer):
275
275
  category_msg.active_days = int(stats["active_days"])
276
276
  category_msg.total_watched = float(stats["total_watched"])
277
277
  category_msg.capped_total_watched = float(stats["capped_total_watched"])
278
+ category_msg.capped_total_log_watched = float(stats.get("capped_total_log_watched", 0.0))
279
+ category_msg.first_active_date = str(stats.get("first_active_date", ""))
278
280
  return root_msg
279
281
 
280
282
 
@@ -295,6 +297,7 @@ class GlobalChannelsSerializerV1(SimpleSerializer):
295
297
  channel_msg.name = str(channel["name"])
296
298
  channel_msg.category_group = str(channel["category_group"])
297
299
  channel_msg.start_date = int(channel["start_date"])
300
+ channel_msg.required_features.extend(str(f) for f in (channel.get("required_features") or []))
298
301
  return root_msg
299
302
 
300
303
 
@@ -0,0 +1 @@
1
+ __version__ = "0.4.10"
@@ -516,6 +516,7 @@ def create_channel_app(
516
516
  )
517
517
 
518
518
  model = state["model"]
519
+ exploration_mode = query_params.get("exploration") in ["True", "true", "1"]
519
520
  try:
520
521
  preprocess_start = time.perf_counter_ns()
521
522
  model["params"]["query_params"] = query_params
@@ -526,6 +527,16 @@ def create_channel_app(
526
527
  )
527
528
  predict_start = time.perf_counter_ns()
528
529
  model_output = model["predict"](model_input, model["params"])
530
+ if exploration_mode:
531
+ if "exploration" not in model:
532
+ raise ValueError("Model does not support exploration mode")
533
+ model_output = model["exploration"](
534
+ user,
535
+ channels,
536
+ model_output,
537
+ model["params"],
538
+ )
539
+
529
540
  predict_end = time.perf_counter_ns()
530
541
  except Exception as e:
531
542
  logger.error("Model prediction failed: \n%s", traceback.format_exc())
@@ -547,7 +558,7 @@ def create_channel_app(
547
558
  "retrieval_ms": retrieval_meta.retrieval_ms,
548
559
  "preprocess_ms": (predict_start - preprocess_start) * 1e-6,
549
560
  "predict_ms": (predict_end - predict_start) * 1e-6,
550
- "total_channels": len(model_output),
561
+ "total_channels": len(model_output) - ("__meta__" in model_output),
551
562
  }
552
563
  newrelic.agent.record_custom_event(
553
564
  "ChannelInference",
@@ -499,31 +499,33 @@ def _process_channels(
499
499
  if playlist_stats_global:
500
500
  for stat in playlist_stats_global.values():
501
501
  all_channels.update(stat.data.keys())
502
- if playlist_stats_user:
503
- for stat in playlist_stats_user.values():
504
- all_channels.update(stat.data.keys())
505
502
 
506
503
  all_channels = all_channels | set(DEFAULT_CHANNELS)
507
504
 
508
- # My headlines will stay on top so not part of the candidates
505
+ # My headlines and local news will stay on top so not part of the candidates
506
+ # watch history and liked videos are always part of the bottom also
509
507
  # The group labels are also ignored, not real channels for UI
510
- ignore_channels = set(["my headlines", "national_favorite", "local_favorite"])
508
+ ignore_channels = {"my headlines", "local news", "national_favorite", "local_favorite", "watch history", "liked videos", "nn_join_us"}
511
509
 
512
- # Rule for weather
513
- if user.get("geo", {}).get("country") != "US":
514
- ignore_channels.add("weather")
515
- # Rule for live_es
516
- if "es" not in user.get("languages", []):
517
- ignore_channels.add("live_es")
510
+ # Rules to filter candidates
511
+ channel_features = user.get("channelFeatures", {})
512
+ channel_eligibility = channel_features.get("eligibility", {})
513
+ channel_feature_predicates = channel_features.get("features", {})
518
514
 
519
515
  inserted_channel_names = set()
520
516
  if channel_candidates is not None:
521
517
  preferred = set(user.get("preferredChannels", []) or [])
522
518
  for ch in channel_candidates.data:
519
+ # Eligibility rules
520
+ if not channel_eligibility.get(ch.name, True):
521
+ ignore_channels.add(ch.name)
522
+ # Feature predicate rules for special channels
523
+ if ch.required_features and not all(channel_feature_predicates.get(f, False) for f in ch.required_features):
524
+ ignore_channels.add(ch.name)
523
525
  # Rule for authors
524
526
  if (
525
527
  ch.category_group in ("national_favorite", "local_favorite")
526
- and ch.name not in preferred
528
+ and (ch.name not in preferred or not channel_eligibility.get(ch.category_group, True))
527
529
  ):
528
530
  ignore_channels.add(ch.name)
529
531
  # Rule for shows
@@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default()
24
24
 
25
25
 
26
26
 
27
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x66\x65\x61tures.proto\x12\x1ahaystack_ml_stack.features\"7\n\x12\x45ntryContextCounts\x12\x10\n\x08\x61ttempts\x18\x01 \x01(\x05\x12\x0f\n\x07watched\x18\x02 \x01(\x05\"_\n\x0cSelectCounts\x12\x15\n\rtotal_selects\x18\x01 \x01(\x05\x12!\n\x19total_selects_and_watched\x18\x02 \x01(\x05\x12\x15\n\rtotal_browsed\x18\x03 \x01(\x05\"\xf3\x02\n\x14\x45ntryContextPWatched\x12@\n\x08\x61utoplay\x18\x01 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12\x41\n\tsel_thumb\x18\x02 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12\x43\n\x0b\x63hoose_next\x18\x03 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12@\n\x08\x63h_swtch\x18\x04 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12O\n\x17launch_first_in_session\x18\x05 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\"\x85\x02\n\x0fPositionPSelect\x12;\n\tfirst_pos\x18\x01 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12<\n\nsecond_pos\x18\x02 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12;\n\tthird_pos\x18\x03 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12:\n\x08rest_pos\x18\x04 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\"\xa9\x01\n\x1f\x42rowsedDebiasedPositionPSelects\x12\x44\n\x0fup_to_4_browsed\x18\x01 \x01(\x0b\x32+.haystack_ml_stack.features.PositionPSelect\x12@\n\x0b\x61ll_browsed\x18\x02 \x01(\x0b\x32+.haystack_ml_stack.features.PositionPSelect\"\xb8\x01\n\x16PlaylistStatsForGlobal\x12\x15\n\rwatched_count\x18\x01 \x01(\x05\x12\x19\n\x11not_watched_count\x18\x02 \x01(\x05\x12\x1b\n\x13\x63\x61pped_watched_secs\x18\x03 \x01(\x02\x12\x1f\n\x17\x63\x61pped_not_watched_secs\x18\x04 \x01(\x02\x12\x14\n\x0cwatched_secs\x18\x05 \x01(\x02\x12\x18\n\x10not_watched_secs\x18\x06 \x01(\x02\"\x88\x01\n\x14PlaylistStatsForUser\x12\x12\n\ntotal_days\x18\x01 \x01(\x05\x12\x12\n\nstart_days\x18\x02 \x01(\x05\x12\x13\n\x0b\x61\x63tive_days\x18\x03 \x01(\x05\x12\x15\n\rtotal_watched\x18\x04 \x01(\x02\x12\x1c\n\x14\x63\x61pped_total_watched\x18\x05 \x01(\x02\"C\n\x07\x43hannel\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x16\n\x0e\x63\x61tegory_group\x18\x02 \x01(\t\x12\x12\n\nstart_date\x18\x03 \x01(\x05\"k\n\rStreamPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12I\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects\"a\n\x0eStreamPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12>\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched\"_\n\x0cUserPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12>\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched\"\xda\x01\n\x19UserPersonalizingPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12M\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32?.haystack_ml_stack.features.UserPersonalizingPWatched.DataEntry\x1a]\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched:\x02\x38\x01\"i\n\x0bUserPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12I\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects\"\xe3\x01\n\x18UserPersonalizingPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12L\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32>.haystack_ml_stack.features.UserPersonalizingPSelect.DataEntry\x1ah\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12J\n\x05value\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects:\x02\x38\x01\"\xa2\x01\n\x16StreamSimilarityScores\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12J\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32<.haystack_ml_stack.features.StreamSimilarityScores.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x01:\x02\x38\x01\"\xd0\x01\n\x13GlobalPlaylistStats\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12G\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x39.haystack_ml_stack.features.GlobalPlaylistStats.DataEntry\x1a_\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x41\n\x05value\x18\x02 \x01(\x0b\x32\x32.haystack_ml_stack.features.PlaylistStatsForGlobal:\x02\x38\x01\"\xca\x01\n\x11UserPlaylistStats\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x45\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x37.haystack_ml_stack.features.UserPlaylistStats.DataEntry\x1a]\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.PlaylistStatsForUser:\x02\x38\x01\"T\n\x0eGlobalChannels\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x31\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32#.haystack_ml_stack.features.Channelb\x06proto3')
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x66\x65\x61tures.proto\x12\x1ahaystack_ml_stack.features\"7\n\x12\x45ntryContextCounts\x12\x10\n\x08\x61ttempts\x18\x01 \x01(\x05\x12\x0f\n\x07watched\x18\x02 \x01(\x05\"_\n\x0cSelectCounts\x12\x15\n\rtotal_selects\x18\x01 \x01(\x05\x12!\n\x19total_selects_and_watched\x18\x02 \x01(\x05\x12\x15\n\rtotal_browsed\x18\x03 \x01(\x05\"\xf3\x02\n\x14\x45ntryContextPWatched\x12@\n\x08\x61utoplay\x18\x01 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12\x41\n\tsel_thumb\x18\x02 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12\x43\n\x0b\x63hoose_next\x18\x03 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12@\n\x08\x63h_swtch\x18\x04 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\x12O\n\x17launch_first_in_session\x18\x05 \x01(\x0b\x32..haystack_ml_stack.features.EntryContextCounts\"\x85\x02\n\x0fPositionPSelect\x12;\n\tfirst_pos\x18\x01 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12<\n\nsecond_pos\x18\x02 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12;\n\tthird_pos\x18\x03 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\x12:\n\x08rest_pos\x18\x04 \x01(\x0b\x32(.haystack_ml_stack.features.SelectCounts\"\xa9\x01\n\x1f\x42rowsedDebiasedPositionPSelects\x12\x44\n\x0fup_to_4_browsed\x18\x01 \x01(\x0b\x32+.haystack_ml_stack.features.PositionPSelect\x12@\n\x0b\x61ll_browsed\x18\x02 \x01(\x0b\x32+.haystack_ml_stack.features.PositionPSelect\"\xb8\x01\n\x16PlaylistStatsForGlobal\x12\x15\n\rwatched_count\x18\x01 \x01(\x05\x12\x19\n\x11not_watched_count\x18\x02 \x01(\x05\x12\x1b\n\x13\x63\x61pped_watched_secs\x18\x03 \x01(\x02\x12\x1f\n\x17\x63\x61pped_not_watched_secs\x18\x04 \x01(\x02\x12\x14\n\x0cwatched_secs\x18\x05 \x01(\x02\x12\x18\n\x10not_watched_secs\x18\x06 \x01(\x02\"\xc5\x01\n\x14PlaylistStatsForUser\x12\x12\n\ntotal_days\x18\x01 \x01(\x05\x12\x12\n\nstart_days\x18\x02 \x01(\x05\x12\x13\n\x0b\x61\x63tive_days\x18\x03 \x01(\x05\x12\x15\n\rtotal_watched\x18\x04 \x01(\x02\x12\x1c\n\x14\x63\x61pped_total_watched\x18\x05 \x01(\x02\x12 \n\x18\x63\x61pped_total_log_watched\x18\x06 \x01(\x02\x12\x19\n\x11\x66irst_active_date\x18\x07 \x01(\t\"^\n\x07\x43hannel\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x16\n\x0e\x63\x61tegory_group\x18\x02 \x01(\t\x12\x12\n\nstart_date\x18\x03 \x01(\x05\x12\x19\n\x11required_features\x18\x04 \x03(\t\"k\n\rStreamPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12I\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects\"a\n\x0eStreamPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12>\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched\"_\n\x0cUserPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12>\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched\"\xda\x01\n\x19UserPersonalizingPWatched\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12M\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32?.haystack_ml_stack.features.UserPersonalizingPWatched.DataEntry\x1a]\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.EntryContextPWatched:\x02\x38\x01\"i\n\x0bUserPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12I\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects\"\xe3\x01\n\x18UserPersonalizingPSelect\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12L\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32>.haystack_ml_stack.features.UserPersonalizingPSelect.DataEntry\x1ah\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12J\n\x05value\x18\x02 \x01(\x0b\x32;.haystack_ml_stack.features.BrowsedDebiasedPositionPSelects:\x02\x38\x01\"\xa2\x01\n\x16StreamSimilarityScores\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12J\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32<.haystack_ml_stack.features.StreamSimilarityScores.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x01:\x02\x38\x01\"\xd0\x01\n\x13GlobalPlaylistStats\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12G\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x39.haystack_ml_stack.features.GlobalPlaylistStats.DataEntry\x1a_\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x41\n\x05value\x18\x02 \x01(\x0b\x32\x32.haystack_ml_stack.features.PlaylistStatsForGlobal:\x02\x38\x01\"\xca\x01\n\x11UserPlaylistStats\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x45\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x37.haystack_ml_stack.features.UserPlaylistStats.DataEntry\x1a]\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.haystack_ml_stack.features.PlaylistStatsForUser:\x02\x38\x01\"T\n\x0eGlobalChannels\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x31\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32#.haystack_ml_stack.features.Channelb\x06proto3')
28
28
 
29
29
  _globals = globals()
30
30
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -54,37 +54,37 @@ if not _descriptor._USE_C_DESCRIPTORS:
54
54
  _globals['_PLAYLISTSTATSFORGLOBAL']._serialized_start=1011
55
55
  _globals['_PLAYLISTSTATSFORGLOBAL']._serialized_end=1195
56
56
  _globals['_PLAYLISTSTATSFORUSER']._serialized_start=1198
57
- _globals['_PLAYLISTSTATSFORUSER']._serialized_end=1334
58
- _globals['_CHANNEL']._serialized_start=1336
59
- _globals['_CHANNEL']._serialized_end=1403
60
- _globals['_STREAMPSELECT']._serialized_start=1405
61
- _globals['_STREAMPSELECT']._serialized_end=1512
62
- _globals['_STREAMPWATCHED']._serialized_start=1514
63
- _globals['_STREAMPWATCHED']._serialized_end=1611
64
- _globals['_USERPWATCHED']._serialized_start=1613
65
- _globals['_USERPWATCHED']._serialized_end=1708
66
- _globals['_USERPERSONALIZINGPWATCHED']._serialized_start=1711
67
- _globals['_USERPERSONALIZINGPWATCHED']._serialized_end=1929
68
- _globals['_USERPERSONALIZINGPWATCHED_DATAENTRY']._serialized_start=1836
69
- _globals['_USERPERSONALIZINGPWATCHED_DATAENTRY']._serialized_end=1929
70
- _globals['_USERPSELECT']._serialized_start=1931
71
- _globals['_USERPSELECT']._serialized_end=2036
72
- _globals['_USERPERSONALIZINGPSELECT']._serialized_start=2039
73
- _globals['_USERPERSONALIZINGPSELECT']._serialized_end=2266
74
- _globals['_USERPERSONALIZINGPSELECT_DATAENTRY']._serialized_start=2162
75
- _globals['_USERPERSONALIZINGPSELECT_DATAENTRY']._serialized_end=2266
76
- _globals['_STREAMSIMILARITYSCORES']._serialized_start=2269
77
- _globals['_STREAMSIMILARITYSCORES']._serialized_end=2431
78
- _globals['_STREAMSIMILARITYSCORES_DATAENTRY']._serialized_start=2388
79
- _globals['_STREAMSIMILARITYSCORES_DATAENTRY']._serialized_end=2431
80
- _globals['_GLOBALPLAYLISTSTATS']._serialized_start=2434
81
- _globals['_GLOBALPLAYLISTSTATS']._serialized_end=2642
82
- _globals['_GLOBALPLAYLISTSTATS_DATAENTRY']._serialized_start=2547
83
- _globals['_GLOBALPLAYLISTSTATS_DATAENTRY']._serialized_end=2642
84
- _globals['_USERPLAYLISTSTATS']._serialized_start=2645
85
- _globals['_USERPLAYLISTSTATS']._serialized_end=2847
86
- _globals['_USERPLAYLISTSTATS_DATAENTRY']._serialized_start=2754
87
- _globals['_USERPLAYLISTSTATS_DATAENTRY']._serialized_end=2847
88
- _globals['_GLOBALCHANNELS']._serialized_start=2849
89
- _globals['_GLOBALCHANNELS']._serialized_end=2933
57
+ _globals['_PLAYLISTSTATSFORUSER']._serialized_end=1395
58
+ _globals['_CHANNEL']._serialized_start=1397
59
+ _globals['_CHANNEL']._serialized_end=1491
60
+ _globals['_STREAMPSELECT']._serialized_start=1493
61
+ _globals['_STREAMPSELECT']._serialized_end=1600
62
+ _globals['_STREAMPWATCHED']._serialized_start=1602
63
+ _globals['_STREAMPWATCHED']._serialized_end=1699
64
+ _globals['_USERPWATCHED']._serialized_start=1701
65
+ _globals['_USERPWATCHED']._serialized_end=1796
66
+ _globals['_USERPERSONALIZINGPWATCHED']._serialized_start=1799
67
+ _globals['_USERPERSONALIZINGPWATCHED']._serialized_end=2017
68
+ _globals['_USERPERSONALIZINGPWATCHED_DATAENTRY']._serialized_start=1924
69
+ _globals['_USERPERSONALIZINGPWATCHED_DATAENTRY']._serialized_end=2017
70
+ _globals['_USERPSELECT']._serialized_start=2019
71
+ _globals['_USERPSELECT']._serialized_end=2124
72
+ _globals['_USERPERSONALIZINGPSELECT']._serialized_start=2127
73
+ _globals['_USERPERSONALIZINGPSELECT']._serialized_end=2354
74
+ _globals['_USERPERSONALIZINGPSELECT_DATAENTRY']._serialized_start=2250
75
+ _globals['_USERPERSONALIZINGPSELECT_DATAENTRY']._serialized_end=2354
76
+ _globals['_STREAMSIMILARITYSCORES']._serialized_start=2357
77
+ _globals['_STREAMSIMILARITYSCORES']._serialized_end=2519
78
+ _globals['_STREAMSIMILARITYSCORES_DATAENTRY']._serialized_start=2476
79
+ _globals['_STREAMSIMILARITYSCORES_DATAENTRY']._serialized_end=2519
80
+ _globals['_GLOBALPLAYLISTSTATS']._serialized_start=2522
81
+ _globals['_GLOBALPLAYLISTSTATS']._serialized_end=2730
82
+ _globals['_GLOBALPLAYLISTSTATS_DATAENTRY']._serialized_start=2635
83
+ _globals['_GLOBALPLAYLISTSTATS_DATAENTRY']._serialized_end=2730
84
+ _globals['_USERPLAYLISTSTATS']._serialized_start=2733
85
+ _globals['_USERPLAYLISTSTATS']._serialized_end=2935
86
+ _globals['_USERPLAYLISTSTATS_DATAENTRY']._serialized_start=2842
87
+ _globals['_USERPLAYLISTSTATS_DATAENTRY']._serialized_end=2935
88
+ _globals['_GLOBALCHANNELS']._serialized_start=2937
89
+ _globals['_GLOBALCHANNELS']._serialized_end=3021
90
90
  # @@protoc_insertion_point(module_scope)
@@ -81,22 +81,28 @@ class PlaylistStatsForUser(_message.Message):
81
81
  ACTIVE_DAYS_FIELD_NUMBER: _ClassVar[int]
82
82
  TOTAL_WATCHED_FIELD_NUMBER: _ClassVar[int]
83
83
  CAPPED_TOTAL_WATCHED_FIELD_NUMBER: _ClassVar[int]
84
+ CAPPED_TOTAL_LOG_WATCHED_FIELD_NUMBER: _ClassVar[int]
85
+ FIRST_ACTIVE_DATE_FIELD_NUMBER: _ClassVar[int]
84
86
  total_days: int
85
87
  start_days: int
86
88
  active_days: int
87
89
  total_watched: float
88
90
  capped_total_watched: float
89
- def __init__(self, total_days: _Optional[int] = ..., start_days: _Optional[int] = ..., active_days: _Optional[int] = ..., total_watched: _Optional[float] = ..., capped_total_watched: _Optional[float] = ...) -> None: ...
91
+ capped_total_log_watched: float
92
+ first_active_date: str
93
+ def __init__(self, total_days: _Optional[int] = ..., start_days: _Optional[int] = ..., active_days: _Optional[int] = ..., total_watched: _Optional[float] = ..., capped_total_watched: _Optional[float] = ..., capped_total_log_watched: _Optional[float] = ..., first_active_date: _Optional[str] = ...) -> None: ...
90
94
 
91
95
  class Channel(_message.Message):
92
96
  __slots__ = ()
93
97
  NAME_FIELD_NUMBER: _ClassVar[int]
94
98
  CATEGORY_GROUP_FIELD_NUMBER: _ClassVar[int]
95
99
  START_DATE_FIELD_NUMBER: _ClassVar[int]
100
+ REQUIRED_FEATURES_FIELD_NUMBER: _ClassVar[int]
96
101
  name: str
97
102
  category_group: str
98
103
  start_date: int
99
- def __init__(self, name: _Optional[str] = ..., category_group: _Optional[str] = ..., start_date: _Optional[int] = ...) -> None: ...
104
+ required_features: _containers.RepeatedScalarFieldContainer[str]
105
+ def __init__(self, name: _Optional[str] = ..., category_group: _Optional[str] = ..., start_date: _Optional[int] = ..., required_features: _Optional[_Iterable[str]] = ...) -> None: ...
100
106
 
101
107
  class StreamPSelect(_message.Message):
102
108
  __slots__ = ()
@@ -715,6 +715,8 @@ def _complete_features_for_channels(
715
715
  "active_days",
716
716
  "total_watched",
717
717
  "capped_total_watched",
718
+ "capped_total_log_watched",
719
+ "first_active_date",
718
720
  ]
719
721
 
720
722
  for ch in channels:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haystack-ml-stack
3
- Version: 0.4.8
3
+ Version: 0.4.10
4
4
  Summary: Functions related to Haystack ML
5
5
  Author-email: Oscar Vega <oscar@haystack.tv>
6
6
  License: MIT
@@ -150,3 +150,64 @@ def test_user_bias_serializers():
150
150
  "User pselect serialization does not match expected value."
151
151
  )
152
152
  return
153
+
154
+
155
+ def test_user_playlist_stats_serializer_backward_compatibility():
156
+ serializer = SerializerRegistry[("USER", "PLAYLIST_CATEGORY_STATS#3M", "v1")]
157
+
158
+ # Case 1: true old binary — built without fields 6 and 7, identical to what
159
+ # the old serializer would have written before the proto change
160
+ old_msg = features_pb2_v1.UserPlaylistStats()
161
+ old_msg.version = 1
162
+ entry = old_msg.data["sports"]
163
+ entry.total_days = 30
164
+ entry.start_days = 5
165
+ entry.active_days = 10
166
+ entry.total_watched = 120.5
167
+ entry.capped_total_watched = 100.0
168
+ old_binary = old_msg.SerializeToString()
169
+
170
+ result = serializer.deserialize(old_binary)
171
+ assert result.data["sports"].total_days == 30
172
+ assert result.data["sports"].capped_total_watched == 100.0
173
+ assert result.data["sports"].capped_total_log_watched == 0.0
174
+ assert result.data["sports"].first_active_date == ""
175
+
176
+ # Case 2: new data with new fields — full round-trip
177
+ new_data = {
178
+ "version": 1,
179
+ "data": {
180
+ "sports": {
181
+ "total_days": 30,
182
+ "start_days": 5,
183
+ "active_days": 10,
184
+ "total_watched": 120.5,
185
+ "capped_total_watched": 100.0,
186
+ "capped_total_log_watched": 4.8,
187
+ "first_active_date": "2024-01-15",
188
+ }
189
+ },
190
+ }
191
+ binary = serializer.serialize(new_data)
192
+ result = serializer.deserialize(binary)
193
+ assert abs(result.data["sports"].capped_total_log_watched - 4.8) < 1e-6
194
+ assert result.data["sports"].first_active_date == "2024-01-15"
195
+
196
+ # Case 3: old-format dict missing the new fields — .get() fallback
197
+ old_format_data = {
198
+ "version": 1,
199
+ "data": {
200
+ "sports": {
201
+ "total_days": 30,
202
+ "start_days": 5,
203
+ "active_days": 10,
204
+ "total_watched": 120.5,
205
+ "capped_total_watched": 100.0,
206
+ }
207
+ },
208
+ }
209
+ binary = serializer.serialize(old_format_data)
210
+ result = serializer.deserialize(binary)
211
+ assert result.data["sports"].total_days == 30
212
+ assert result.data["sports"].capped_total_log_watched == 0.0
213
+ assert result.data["sports"].first_active_date == ""
@@ -1 +0,0 @@
1
- __version__ = "0.4.8"