AsamAce commited on
Commit
658bb70
·
verified ·
1 Parent(s): da2b974

Upload handler.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. handler.py +53 -0
handler.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Inference Endpoints custom handler.
3
+
4
+ Deliberately narrow: this endpoint scores an ALREADY-EXTRACTED feature vector — it
5
+ does not accept or walk a raw serialization graph. Graph feature extraction stays
6
+ single-sourced in the TS engine (packages/intelligence/src/features.ts); see
7
+ docs/huggingface-model-plan.md §0 for why duplicating that traversal here would be
8
+ an unacceptable drift risk for a system whose whole premise is "never fabricate."
9
+
10
+ Expected input (feature order MUST match model.json's `featureNames`):
11
+ {"inputs": {"features": [0.12, 3, 1, 2, 1, 1, 5, 0.0]}}
12
+ or a batch:
13
+ {"inputs": [{"features": [...]}, {"features": [...]}]}
14
+
15
+ Output mirrors the TS AnomalyOutput contract exactly (see scorer.py / model.ts):
16
+ {"score": 0.87, "uncertainty": 0.05, "explanation": "learned anomaly 87% (top: route_rarity)"}
17
+ """
18
+
19
+ from __future__ import annotations
20
+ import os
21
+ from typing import Any
22
+
23
+ from scorer import load_model, score_features
24
+
25
+
26
+ class EndpointHandler:
27
+ def __init__(self, path: str = ""):
28
+ model_path = os.path.join(path, "model.json") if path else "model.json"
29
+ self.model = load_model(model_path)
30
+ self.feature_names: list[str] = self.model.get("featureNames", [])
31
+ self.expected_len = len(self.feature_names) or len(self.model["weights"])
32
+
33
+ def _score_one(self, item: dict) -> dict:
34
+ features = item.get("features")
35
+ if not isinstance(features, list) or len(features) != self.expected_len:
36
+ # Never guess: an out-of-contract input abstains completely rather than
37
+ # scoring garbage, mirroring the engine's "abstain when uncertain" rule.
38
+ return {
39
+ "score": 0.0,
40
+ "uncertainty": 1.0,
41
+ "explanation": (
42
+ f"invalid input: expected a 'features' array of length {self.expected_len} "
43
+ f"({', '.join(self.feature_names)}) -- model abstains"
44
+ ),
45
+ }
46
+ out = score_features(self.model, [float(x) for x in features])
47
+ return out.to_dict()
48
+
49
+ def __call__(self, data: dict[str, Any]) -> list[dict] | dict:
50
+ inputs = data.get("inputs", data)
51
+ if isinstance(inputs, list):
52
+ return [self._score_one(item) for item in inputs]
53
+ return self._score_one(inputs)