haystack-ml-stack 0.3.4__tar.gz → 0.4.1__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.
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/PKG-INFO +3 -1
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/pyproject.toml +3 -2
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/__init__.py +3 -3
- haystack_ml_stack-0.4.1/src/haystack_ml_stack/_kafka.py +88 -0
- haystack_ml_stack-0.4.1/src/haystack_ml_stack/_version.py +1 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/app.py +51 -18
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/settings.py +8 -2
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack.egg-info/PKG-INFO +3 -1
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack.egg-info/SOURCES.txt +2 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack.egg-info/requires.txt +2 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/README.md +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/setup.cfg +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/_serializers.py +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/cache.py +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/dynamo.py +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/exceptions.py +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/generated/__init__.py +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/generated/v1/__init__.py +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/generated/v1/features_pb2.py +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/generated/v1/features_pb2.pyi +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/model_store.py +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/utils.py +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack.egg-info/dependency_links.txt +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack.egg-info/top_level.txt +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/tests/test_serializers.py +0 -0
- {haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/tests/test_utils.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: haystack-ml-stack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Functions related to Haystack ML
|
|
5
5
|
Author-email: Oscar Vega <oscar@haystack.tv>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.11
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: protobuf==6.33.2
|
|
10
|
+
Requires-Dist: orjson==3.11.7
|
|
10
11
|
Provides-Extra: server
|
|
11
12
|
Requires-Dist: pydantic==2.5.0; extra == "server"
|
|
12
13
|
Requires-Dist: cachetools==5.5.2; extra == "server"
|
|
@@ -15,6 +16,7 @@ Requires-Dist: aioboto3==12.0.0; extra == "server"
|
|
|
15
16
|
Requires-Dist: fastapi==0.104.1; extra == "server"
|
|
16
17
|
Requires-Dist: pydantic-settings==2.2; extra == "server"
|
|
17
18
|
Requires-Dist: newrelic==11.1.0; extra == "server"
|
|
19
|
+
Requires-Dist: confluent-kafka==2.13.0; extra == "server"
|
|
18
20
|
|
|
19
21
|
# Haystack ML Stack
|
|
20
22
|
|
|
@@ -5,13 +5,13 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "haystack-ml-stack"
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.4.1"
|
|
9
9
|
description = "Functions related to Haystack ML"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
authors = [{ name = "Oscar Vega", email = "oscar@haystack.tv" }]
|
|
12
12
|
requires-python = ">=3.11"
|
|
13
13
|
dependencies = [
|
|
14
|
-
"protobuf==6.33.2",
|
|
14
|
+
"protobuf==6.33.2", "orjson==3.11.7"
|
|
15
15
|
]
|
|
16
16
|
license = { text = "MIT" }
|
|
17
17
|
|
|
@@ -24,4 +24,5 @@ server = [
|
|
|
24
24
|
"fastapi==0.104.1",
|
|
25
25
|
"pydantic-settings==2.2",
|
|
26
26
|
"newrelic==11.1.0",
|
|
27
|
+
"confluent-kafka==2.13.0"
|
|
27
28
|
]
|
|
@@ -4,11 +4,11 @@ try:
|
|
|
4
4
|
from .app import create_app
|
|
5
5
|
|
|
6
6
|
__all__ = ["create_app"]
|
|
7
|
-
except ImportError:
|
|
7
|
+
except ImportError as e:
|
|
8
8
|
pass
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
from ._serializers import SerializerRegistry, FeatureRegistryId
|
|
12
|
+
from ._version import __version__
|
|
11
13
|
|
|
12
14
|
__all__ = [*__all__, "SerializerRegistry", "FeatureRegistryId"]
|
|
13
|
-
|
|
14
|
-
__version__ = "0.3.4"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from confluent_kafka.aio import AIOProducer
|
|
2
|
+
import orjson
|
|
3
|
+
from google.protobuf.message import Message
|
|
4
|
+
import base64
|
|
5
|
+
import os
|
|
6
|
+
import logging
|
|
7
|
+
from .settings import Settings
|
|
8
|
+
from ._version import __version__
|
|
9
|
+
import hashlib
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
SECURITY_PROTOCOL = "SASL_SSL"
|
|
13
|
+
SASL_MECHANISM = "SCRAM-SHA-512"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def send_to_kafka(
|
|
17
|
+
producer: AIOProducer,
|
|
18
|
+
topic: str,
|
|
19
|
+
user: dict,
|
|
20
|
+
streams: list[dict],
|
|
21
|
+
playlist: dict,
|
|
22
|
+
state: dict,
|
|
23
|
+
model_output: dict,
|
|
24
|
+
monitoring_meta: dict,
|
|
25
|
+
) -> None:
|
|
26
|
+
if topic is None or producer is None:
|
|
27
|
+
return
|
|
28
|
+
message = {
|
|
29
|
+
"userid": user.get("userid"),
|
|
30
|
+
"client_os": playlist.get("clientOs"),
|
|
31
|
+
"model_input": {"user": user, "streams": streams, "playlist": playlist},
|
|
32
|
+
"model_output": model_output,
|
|
33
|
+
"model_name": state["model_name"].replace(".pkl", "")
|
|
34
|
+
if state["model_name"]
|
|
35
|
+
else None,
|
|
36
|
+
"model_type": "streams",
|
|
37
|
+
"meta": {
|
|
38
|
+
"monitoring": monitoring_meta,
|
|
39
|
+
"haystack_ml_stack_version": __version__,
|
|
40
|
+
"playlist_category": playlist.get("category"),
|
|
41
|
+
"user_features": state.get("user_features", []),
|
|
42
|
+
"stream_features": state.get("stream_features", []),
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
delivery_future = await producer.produce(
|
|
46
|
+
topic, orjson.dumps(message, default=default_serialization)
|
|
47
|
+
)
|
|
48
|
+
await delivery_future
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def default_serialization(obj):
|
|
53
|
+
if isinstance(obj, Message):
|
|
54
|
+
return {
|
|
55
|
+
"version": obj.version,
|
|
56
|
+
"proto": base64.b64encode(obj.SerializeToString()).decode("ascii"),
|
|
57
|
+
}
|
|
58
|
+
raise orjson.JSONEncodeError("Unknown data type to serialize!")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def initialize_kafka_producer(app_config: Settings) -> AIOProducer:
|
|
62
|
+
secret_keys = orjson.loads(os.getenv("SECRET_KEYS") or "{}")
|
|
63
|
+
if not secret_keys:
|
|
64
|
+
raise ValueError("No Kafka credentials found.")
|
|
65
|
+
with open("/tmp/ca.pem", "w") as f:
|
|
66
|
+
f.write(base64.b64decode(secret_keys["KAFKA_BROKER_CA_CERTIFICATE"]).decode())
|
|
67
|
+
kafka_config = {
|
|
68
|
+
"bootstrap.servers": app_config.kafka_bootstrap_servers,
|
|
69
|
+
"security.protocol": SECURITY_PROTOCOL,
|
|
70
|
+
"sasl.username": secret_keys["KAFKA_WRITER_USER"],
|
|
71
|
+
"sasl.password": secret_keys["KAFKA_WRITER_PASSWORD"],
|
|
72
|
+
"sasl.mechanism": SASL_MECHANISM,
|
|
73
|
+
"ssl.ca.location": "/tmp/ca.pem",
|
|
74
|
+
"compression.type": "lz4",
|
|
75
|
+
}
|
|
76
|
+
logger.info(
|
|
77
|
+
"Initializing kafka producer pushing to topic %s", app_config.kafka_topic
|
|
78
|
+
)
|
|
79
|
+
producer = AIOProducer(kafka_config)
|
|
80
|
+
logger.info("Producer initialized!")
|
|
81
|
+
return producer
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def should_log_user(userid: str, kafka_fraction: float) -> bool:
|
|
85
|
+
if not userid:
|
|
86
|
+
return False
|
|
87
|
+
hash_value = int(hashlib.sha256(userid.encode()).hexdigest(), 16) / (2**256)
|
|
88
|
+
return hash_value < kafka_fraction
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.1"
|
|
@@ -8,20 +8,21 @@ import time
|
|
|
8
8
|
from contextlib import asynccontextmanager, AsyncExitStack
|
|
9
9
|
import traceback
|
|
10
10
|
import json
|
|
11
|
+
import asyncio
|
|
11
12
|
|
|
12
13
|
import aiobotocore.session
|
|
13
14
|
from aiobotocore.config import AioConfig
|
|
14
|
-
from fastapi import FastAPI, HTTPException, Request, Response
|
|
15
|
+
from fastapi import FastAPI, HTTPException, Request, Response, BackgroundTasks
|
|
15
16
|
from fastapi.encoders import jsonable_encoder
|
|
16
17
|
import newrelic.agent
|
|
17
18
|
|
|
18
|
-
|
|
19
19
|
from .cache import make_features_cache
|
|
20
20
|
from .dynamo import set_all_features, FeatureRetrievalMeta
|
|
21
21
|
from .model_store import download_and_load_model
|
|
22
22
|
from .settings import Settings
|
|
23
23
|
from . import exceptions
|
|
24
24
|
from ._serializers import SerializerRegistry
|
|
25
|
+
from ._kafka import send_to_kafka, initialize_kafka_producer, should_log_user
|
|
25
26
|
from google.protobuf import text_format
|
|
26
27
|
|
|
27
28
|
logging.basicConfig(
|
|
@@ -123,6 +124,10 @@ def create_app(
|
|
|
123
124
|
# 1. Load ML Model
|
|
124
125
|
if state["model"] is None:
|
|
125
126
|
await load_model(state, cfg)
|
|
127
|
+
kafka_producer = None
|
|
128
|
+
if cfg.kafka_bootstrap_servers is not None:
|
|
129
|
+
kafka_producer = initialize_kafka_producer(app_config=cfg)
|
|
130
|
+
state["kafka_producer"] = kafka_producer
|
|
126
131
|
async with AsyncExitStack() as stack:
|
|
127
132
|
# 2. Initialize DynamoDB Client (Persistent Pool)
|
|
128
133
|
session = state["session"]
|
|
@@ -139,6 +144,17 @@ def create_app(
|
|
|
139
144
|
# 3. Shutdown Logic
|
|
140
145
|
# The AsyncExitStack automatically closes the DynamoDB client pool here
|
|
141
146
|
logger.info("Shutting down: Connection pools closed.")
|
|
147
|
+
logger.info("Shutting down: Flushing Kafka queue.")
|
|
148
|
+
if kafka_producer is not None:
|
|
149
|
+
try:
|
|
150
|
+
await kafka_producer.flush()
|
|
151
|
+
except Exception:
|
|
152
|
+
logger.error(
|
|
153
|
+
"Unknown exception while flushing kafka queue, shutting down producer.\n%s",
|
|
154
|
+
traceback.format_exc(),
|
|
155
|
+
)
|
|
156
|
+
finally:
|
|
157
|
+
await kafka_producer.close()
|
|
142
158
|
|
|
143
159
|
app = FastAPI(
|
|
144
160
|
title="ML Stream Scorer",
|
|
@@ -166,7 +182,9 @@ def create_app(
|
|
|
166
182
|
}
|
|
167
183
|
|
|
168
184
|
@app.post("/score", status_code=HTTPStatus.OK)
|
|
169
|
-
async def score_stream(
|
|
185
|
+
async def score_stream(
|
|
186
|
+
request: Request, response: Response, background_tasks: BackgroundTasks
|
|
187
|
+
):
|
|
170
188
|
if state["model"] is None:
|
|
171
189
|
raise HTTPException(
|
|
172
190
|
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
|
@@ -187,7 +205,8 @@ def create_app(
|
|
|
187
205
|
) from e
|
|
188
206
|
except Exception as e:
|
|
189
207
|
logger.error(
|
|
190
|
-
"Unexpected exception when parsing request.\n %s",
|
|
208
|
+
"Unexpected exception when parsing request.\n %s",
|
|
209
|
+
traceback.format_exc(),
|
|
191
210
|
)
|
|
192
211
|
raise HTTPException(
|
|
193
212
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Unknown exception"
|
|
@@ -274,22 +293,24 @@ def create_app(
|
|
|
274
293
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
275
294
|
detail="Model prediction failed",
|
|
276
295
|
) from e
|
|
277
|
-
|
|
296
|
+
monitoring_meta = {
|
|
297
|
+
"cache_misses": retrieval_meta.cache_misses,
|
|
298
|
+
"user_cache_misses": retrieval_meta.user_cache_misses,
|
|
299
|
+
"stream_cache_misses": retrieval_meta.stream_cache_misses,
|
|
300
|
+
"user_cache_size": len(user_features_cache),
|
|
301
|
+
"stream_cache_size": len(stream_features_cache),
|
|
302
|
+
"retrieval_success": int(retrieval_meta.success),
|
|
303
|
+
"cache_delay_minutes": retrieval_meta.cache_delay_minutes,
|
|
304
|
+
"dynamo_ms": retrieval_meta.dynamo_ms,
|
|
305
|
+
"dynamo_parse_ms": retrieval_meta.parsing_ms,
|
|
306
|
+
"retrieval_ms": retrieval_meta.retrieval_ms,
|
|
307
|
+
"preprocess_ms": (predict_start - preprocess_start) * 1e-6,
|
|
308
|
+
"predict_ms": (predict_end - predict_start) * 1e-6,
|
|
309
|
+
"total_streams": len(model_output),
|
|
310
|
+
}
|
|
278
311
|
newrelic.agent.record_custom_event(
|
|
279
312
|
"Inference",
|
|
280
|
-
|
|
281
|
-
"cache_misses": retrieval_meta.cache_misses,
|
|
282
|
-
"user_cache_misses": retrieval_meta.user_cache_misses,
|
|
283
|
-
"stream_cache_misses": retrieval_meta.stream_cache_misses,
|
|
284
|
-
"retrieval_success": int(retrieval_meta.success),
|
|
285
|
-
"cache_delay_minutes": retrieval_meta.cache_delay_minutes,
|
|
286
|
-
"dynamo_ms": retrieval_meta.dynamo_ms,
|
|
287
|
-
"dynamo_parse_ms": retrieval_meta.parsing_ms,
|
|
288
|
-
"retrieval_ms": retrieval_meta.retrieval_ms,
|
|
289
|
-
"preprocess_ms": (predict_start - preprocess_start) * 1e-6,
|
|
290
|
-
"predict_ms": (predict_end - predict_start) * 1e-6,
|
|
291
|
-
"total_streams": len(model_output),
|
|
292
|
-
},
|
|
313
|
+
monitoring_meta,
|
|
293
314
|
)
|
|
294
315
|
if model_output:
|
|
295
316
|
if random_number < cfg.logs_fraction:
|
|
@@ -298,6 +319,18 @@ def create_app(
|
|
|
298
319
|
userid,
|
|
299
320
|
model_output,
|
|
300
321
|
)
|
|
322
|
+
if should_log_user(userid=userid, kafka_fraction=cfg.kafka_fraction):
|
|
323
|
+
background_tasks.add_task(
|
|
324
|
+
send_to_kafka,
|
|
325
|
+
producer=state["kafka_producer"],
|
|
326
|
+
topic=cfg.kafka_topic,
|
|
327
|
+
user=user,
|
|
328
|
+
streams=streams,
|
|
329
|
+
playlist=playlist,
|
|
330
|
+
state=state,
|
|
331
|
+
model_output=model_output,
|
|
332
|
+
monitoring_meta=monitoring_meta,
|
|
333
|
+
)
|
|
301
334
|
return jsonable_encoder(model_output)
|
|
302
335
|
|
|
303
336
|
raise HTTPException(
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
from pydantic_settings import BaseSettings
|
|
2
2
|
from pydantic import Field
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
class Settings(BaseSettings):
|
|
5
6
|
# Logging
|
|
6
7
|
logs_fraction: float = Field(0.01, alias="LOGS_FRACTION")
|
|
8
|
+
kafka_bootstrap_servers: str | None = Field(
|
|
9
|
+
default=None, alias="KAFKA_BOOTSTRAP_SERVERS"
|
|
10
|
+
)
|
|
11
|
+
kafka_fraction: float = Field(0.01, alias="KAFKA_FRACTION")
|
|
12
|
+
kafka_topic: str = Field(default=None, alias="KAFKA_TOPIC")
|
|
7
13
|
|
|
8
14
|
# Model (S3)
|
|
9
15
|
s3_model_path: str | None = Field(default=None, alias="S3_MODEL_PATH")
|
|
@@ -14,10 +20,10 @@ class Settings(BaseSettings):
|
|
|
14
20
|
|
|
15
21
|
# Cache
|
|
16
22
|
stream_cache_maxsize: int = 50_000
|
|
17
|
-
user_cache_maxsize: int =
|
|
23
|
+
user_cache_maxsize: int = 80_000
|
|
18
24
|
cache_separator: str = "--"
|
|
19
25
|
|
|
20
26
|
class Config:
|
|
21
27
|
env_file = ".env"
|
|
22
28
|
env_file_encoding = "utf-8"
|
|
23
|
-
extra = "ignore"
|
|
29
|
+
extra = "ignore"
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: haystack-ml-stack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Functions related to Haystack ML
|
|
5
5
|
Author-email: Oscar Vega <oscar@haystack.tv>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.11
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: protobuf==6.33.2
|
|
10
|
+
Requires-Dist: orjson==3.11.7
|
|
10
11
|
Provides-Extra: server
|
|
11
12
|
Requires-Dist: pydantic==2.5.0; extra == "server"
|
|
12
13
|
Requires-Dist: cachetools==5.5.2; extra == "server"
|
|
@@ -15,6 +16,7 @@ Requires-Dist: aioboto3==12.0.0; extra == "server"
|
|
|
15
16
|
Requires-Dist: fastapi==0.104.1; extra == "server"
|
|
16
17
|
Requires-Dist: pydantic-settings==2.2; extra == "server"
|
|
17
18
|
Requires-Dist: newrelic==11.1.0; extra == "server"
|
|
19
|
+
Requires-Dist: confluent-kafka==2.13.0; extra == "server"
|
|
18
20
|
|
|
19
21
|
# Haystack ML Stack
|
|
20
22
|
|
{haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack.egg-info/SOURCES.txt
RENAMED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
src/haystack_ml_stack/__init__.py
|
|
4
|
+
src/haystack_ml_stack/_kafka.py
|
|
4
5
|
src/haystack_ml_stack/_serializers.py
|
|
6
|
+
src/haystack_ml_stack/_version.py
|
|
5
7
|
src/haystack_ml_stack/app.py
|
|
6
8
|
src/haystack_ml_stack/cache.py
|
|
7
9
|
src/haystack_ml_stack/dynamo.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/generated/__init__.py
RENAMED
|
File without changes
|
{haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack/generated/v1/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{haystack_ml_stack-0.3.4 → haystack_ml_stack-0.4.1}/src/haystack_ml_stack.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|