| 125 | |
| 126 | |
| 127 | class EmbeddingSignal: |
| 128 | def __init__( |
| 129 | self, |
| 130 | index_path: Path | None = None, |
| 131 | labels_path: Path | None = None, |
| 132 | model_name: str | None = "BAAI/bge-small-en-v1.5", |
| 133 | classifier_path: Path | None = None, |
| 134 | use_classifier: bool = True, |
| 135 | classifier_fallback_threshold: float = 0.92, |
| 136 | ): |
| 137 | self._embeddings: Any = None |
| 138 | self._labels: list[int] | None = None |
| 139 | self._embed_fn: Callable | None = None |
| 140 | self._classifier: Any = None # sklearn classifier (optional) |
| 141 | self._meta_scaler: Any = None # StandardScaler for metadata features |
| 142 | self._clf_fallback_threshold = classifier_fallback_threshold |
| 143 | |
| 144 | if np is None: |
| 145 | logger.info("numpy not installed; embedding signal disabled") |
| 146 | return |
| 147 | |
| 148 | if index_path and Path(index_path).exists() and labels_path and Path(labels_path).exists(): |
| 149 | self._embeddings = np.load(index_path) |
| 150 | with open(labels_path, encoding="utf-8") as f: |
| 151 | self._labels = json.load(f) |
| 152 | logger.info(f"Loaded embedding index: {len(self._labels)} vectors") |
| 153 | |
| 154 | # Try to load trained classifier (skip entirely when use_classifier=False) |
| 155 | if use_classifier: |
| 156 | if classifier_path and Path(classifier_path).exists(): |
| 157 | try: |
| 158 | with open(classifier_path, "rb") as f: |
| 159 | self._classifier = pickle.load(f) |
| 160 | logger.info("Loaded trained embedding classifier from %s", classifier_path) |
| 161 | self._try_load_scaler(Path(classifier_path).parent) |
| 162 | except Exception as e: |
| 163 | logger.warning("Failed to load classifier: %s — falling back to KNN", e) |
| 164 | elif index_path: |
| 165 | # Auto-detect classifier next to the index |
| 166 | auto_clf = Path(index_path).parent / "embedding_classifier.pkl" |
| 167 | if auto_clf.exists(): |
| 168 | try: |
| 169 | with open(auto_clf, "rb") as f: |
| 170 | self._classifier = pickle.load(f) |
| 171 | logger.info("Auto-loaded embedding classifier from %s", auto_clf) |
| 172 | self._try_load_scaler(Path(index_path).parent) |
| 173 | except Exception as e: |
| 174 | logger.warning("Failed to auto-load embedding classifier from %s: %s", auto_clf, e) |
| 175 | |
| 176 | if model_name: |
| 177 | try: |
| 178 | from sentence_transformers import SentenceTransformer |
| 179 | model = SentenceTransformer(model_name) |
| 180 | self._embed_fn = lambda text: model.encode(text, normalize_embeddings=True) |
| 181 | except ImportError: |
| 182 | logger.warning("sentence-transformers not installed; embedding signal will abstain") |
| 183 | except Exception as e: |
| 184 | logger.warning(f"Failed to load embedding model {model_name}: {e}") |
no outgoing calls