AbstractIntegratedModule 0.6.5__tar.gz → 0.6.7__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 (21) hide show
  1. abstractintegratedmodule-0.6.7/AbstractIntegratedModule.cp313-win_amd64.pyd +0 -0
  2. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractIntegratedModule.cpython-310-aarch64-linux-gnu.so +0 -0
  3. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractIntegratedModule.cpython-312-x86_64-linux-gnu.so +0 -0
  4. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7/AbstractIntegratedModule.egg-info}/PKG-INFO +6 -6
  5. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractIntegratedModule.py +211 -93
  6. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractOptimizedModules.c +152 -152
  7. {abstractintegratedmodule-0.6.5/AbstractIntegratedModule.egg-info → abstractintegratedmodule-0.6.7}/PKG-INFO +6 -6
  8. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/README.md +5 -5
  9. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/setup.py +1 -1
  10. abstractintegratedmodule-0.6.5/AbstractIntegratedModule.cp313-win_amd64.pyd +0 -0
  11. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractIntegratedModule.egg-info/SOURCES.txt +0 -0
  12. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractIntegratedModule.egg-info/dependency_links.txt +0 -0
  13. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractIntegratedModule.egg-info/requires.txt +0 -0
  14. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractIntegratedModule.egg-info/top_level.txt +0 -0
  15. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractOptimizedModules.cp313-win_amd64.pyd +0 -0
  16. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractOptimizedModules.cpython-310-aarch64-linux-gnu.so +0 -0
  17. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractOptimizedModules.cpython-312-x86_64-linux-gnu.so +0 -0
  18. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/AbstractOptimizedModules.pyx +0 -0
  19. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/MANIFEST.in +0 -0
  20. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/pyproject.toml +0 -0
  21. {abstractintegratedmodule-0.6.5 → abstractintegratedmodule-0.6.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AbstractIntegratedModule
3
- Version: 0.6.5
3
+ Version: 0.6.7
4
4
  Summary: Library for Advanced Integrated Non-LLM AI Models - Optimized Backend Framework For Non-LLM AI Agent
5
5
  Author: Micro-Novelty
6
6
  Author-email: hernikpuspita5@gmail.com
@@ -42,7 +42,7 @@ https://github.com/Micro-Novelty/IntegratedPipeline-Specialized-Non-LLM-AI-Agent
42
42
  #### Note: The README here you are reading is a direct copy from my README Repository, to download the necessary files, you can visit my Repository with the provided link above.
43
43
 
44
44
  ### Library Short Description:
45
- - Development Stage: 0.6.5 Official Release.
45
+ - Development Stage: 0.6.7 Official Release.
46
46
  - Maintainer: Micro-Novelty.
47
47
  - library Source-Code is Open-sourced on github.
48
48
  - Purpose: Specifically Designed for providing Non-LLM AI Agent Framework for edge Devices, Optimized for ARM64 architecture.
@@ -67,11 +67,11 @@ https://github.com/Micro-Novelty/IntegratedPipeline-Specialized-Non-LLM-AI-Agent
67
67
  - Robust Advanced prediction capabilities proven effective on ARM64 Using MLP + LSTM Architectures.
68
68
  - Transformer Modules Optimized using Cython, to reduce Memory overhead and Reduce CPU Usage, With Reduced Training Time.
69
69
  - Changelog:
70
- - v0.6.5:
70
+ - v0.6.7:
71
71
  - [=] New features:
72
- - New architecture: AccuracyAnswerCache
73
- - Purpose: Storing correct answered problem for better reliability in Non-dynamic environment, triggered after the model answered correctly and initiate a new prediction, the model will choose the previous correct answer when samples match.
74
- - Fixed edge case where shape mismatch bug happens during MLP Training.
72
+ - Fixed bug where mlp always return index 0 during prediction.
73
+ - Optimized processing batch in chunk function.
74
+ - fixed bug where num_classes doesnt match given labels.
75
75
  -----
76
76
 
77
77
  <img width="1280" height="600" alt="WhatsApp Image 2026-05-27 at 07 16 32" src="https://github.com/user-attachments/assets/4b58a556-45a3-419b-96fd-9c1b76cac574" />
@@ -1684,7 +1684,7 @@ class MLP:
1684
1684
 
1685
1685
  def focused_forward(self, x):
1686
1686
  for layer in self.feed_layers:
1687
- x = layer.forward(x)
1687
+ x = layer.forward(np.asarray(x, dtype=np.float64))
1688
1688
 
1689
1689
  return self.softmax.forward(x)
1690
1690
 
@@ -1693,8 +1693,31 @@ class MLP:
1693
1693
  x = layer.forward(x)
1694
1694
 
1695
1695
  return self.softmax.forward(x)
1696
-
1697
- def focused_backward(self, grad, lr):
1696
+
1697
+
1698
+ def _calibrate_gradient(self, grad, AME, anisotropy):
1699
+ std = np.std(grad)
1700
+ eps = 1e-5
1701
+ calibration = grad.copy()
1702
+
1703
+ if std > 0.1:
1704
+ AEL = (1.0 - AME) * anisotropy # abstraction enviromental limit
1705
+ PRA = (1.0 - AEL) * std # possible reflected abstraction
1706
+
1707
+ calibration = grad * AEL * PRA
1708
+ calibration /= np.sum(calibration)
1709
+
1710
+ calibration = np.asarray(calibration, dtype=np.float64)
1711
+ if np.isnan(calibration).any() or np.isinf(calibration).any():
1712
+ calibration = grad.copy()
1713
+
1714
+ return calibration
1715
+
1716
+
1717
+
1718
+
1719
+ def focused_backward(self, grad, lr, AME, anisotropy):
1720
+ grad = self._calibrate_gradient(grad, AME, anisotropy)
1698
1721
  grad = self.softmax.backward(grad)
1699
1722
  for layer in reversed(self.feed_layers):
1700
1723
  grad = layer.backward(grad, lr)
@@ -1788,7 +1811,9 @@ class MLP:
1788
1811
  return y_pred, y_true
1789
1812
 
1790
1813
  def train(self, X, y, epochs=1000, lr=0.01, verbose=True):
1791
- focused_fit_condition = len(self.feed_layers) > 0 and self.anisotropy_measurement(X) > 0.25 and self.AME_Encoder(X) > 0.25
1814
+ AME = self.AME_Encoder(X)
1815
+ anisotropy = self.anisotropy_measurement(X)
1816
+ focused_fit_condition = len(self.feed_layers) > 0 and anisotropy > 0.25 and AME > 0.25
1792
1817
  print(f'[+] Focused fit condition: {focused_fit_condition} || Anisotropy: {self.anisotropy_measurement(X):.4f} || AME: {self.AME_Encoder(X):.4f}')
1793
1818
 
1794
1819
  for epoch in range(epochs):
@@ -1800,7 +1825,10 @@ class MLP:
1800
1825
  y_pred, y_true = self.adapt_predict_shape(y_pred, y)
1801
1826
  loss = Loss.categorical_crossentropy(y_true, y_pred)
1802
1827
  grad = Loss.softmax_crossentropy_derivative(y_true, y_pred)
1803
- _ = self.backward(grad, self.lr)
1828
+ if focused_fit_condition:
1829
+ _ = self.focused_backward(grad, self.lr, AME, anisotropy)
1830
+ else:
1831
+ _ = self.backward(grad, self.lr)
1804
1832
 
1805
1833
  if verbose and epoch % 100 == 0:
1806
1834
  acc = np.mean(np.argmax(y_pred, axis=1) == np.argmax(y_true, axis=1))
@@ -8131,9 +8159,42 @@ class IntegratedPipeline:
8131
8159
  def initialize_model_encoding(self, X, y_raw):
8132
8160
  vocab_size = self.vocab_size
8133
8161
 
8134
- num_classes = len(np.unique(y_raw))
8162
+ # canonical source first, since model output dimension
8163
+ # (once it exists) is the true authority on num_classes
8164
+ num_classes = self._get_num_classes()
8165
+
8166
+ unique_in_batch = len(np.unique(y_raw))
8167
+
8168
+ if num_classes is None:
8169
+ # no model exists yet — this is the legitimate case where
8170
+ # inferring from y_raw is correct (e.g. first-time initialization)
8171
+ num_classes = unique_in_batch
8172
+ print(f'[=] No existing model — initializing with {num_classes} classes '
8173
+ f'from this batch')
8174
+ elif unique_in_batch > num_classes:
8175
+ # batch contains MORE classes than the model supports
8176
+ # this is a real problem — can't onehot-encode into a smaller space
8177
+ print(f'[⚠️] Batch contains {unique_in_batch} unique classes but model '
8178
+ f'only supports {num_classes} — expanding to {unique_in_batch}')
8179
+ num_classes = unique_in_batch
8180
+ elif unique_in_batch < num_classes:
8181
+ # batch just doesn't happen to contain all classes — this is FINE,
8182
+ # use the model's full num_classes so onehot stays the right width
8183
+ print(f'[=] Batch only contains {unique_in_batch}/{num_classes} classes '
8184
+ f'— using full model class count for onehot width')
8185
+ # num_classes stays as the model's true value, no change needed
8186
+
8135
8187
  y_onehot = np.zeros((len(y_raw), num_classes))
8136
- y_onehot[np.arange(len(y_raw)), y_raw] = 1
8188
+
8189
+ for idx, label in enumerate(y_raw):
8190
+ label_idx = int(label)
8191
+ if 0 <= label_idx < num_classes:
8192
+ y_onehot[idx, label_idx] = 1.0
8193
+ else:
8194
+ print(f'[⚠️] Label {label_idx} out of range for {num_classes} classes '
8195
+ f'at sample {idx} — skipping onehot assignment')
8196
+ # leaves that row as all-zeros rather than crashing or
8197
+ # silently assigning to a wrong class
8137
8198
 
8138
8199
  automatic_change = self.automatic_parameterization(vocab_size, num_classes)
8139
8200
  self.embedding_dim = automatic_change
@@ -8144,6 +8205,9 @@ class IntegratedPipeline:
8144
8205
  model.add(layer1)
8145
8206
  model.add(layer2)
8146
8207
 
8208
+ model.feed_add(layer1)
8209
+ model.feed_add(layer2)
8210
+
8147
8211
  return y_onehot
8148
8212
 
8149
8213
 
@@ -8155,8 +8219,8 @@ class IntegratedPipeline:
8155
8219
 
8156
8220
  layer1= Dense(X, input_dim, automatic_change, activation="relu")
8157
8221
  layer2 = Dense(X, automatic_change, num_classes, activation='relu')
8158
-
8159
- abundant_layer = automatic_change * 10
8222
+
8223
+ abundant_layer = int(automatic_change * 10)
8160
8224
  first_feed_layer = Dense(X, input_dim, abundant_layer, activation="relu")
8161
8225
  sec_feed_layer = Dense(X, abundant_layer, num_classes, activation="relu")
8162
8226
 
@@ -8165,8 +8229,8 @@ class IntegratedPipeline:
8165
8229
  self.model3.add(layer1)
8166
8230
  self.model3.add(layer2)
8167
8231
 
8168
- self.focused_mlp.feed_add(first_feed_layer)
8169
- self.focused_mlp.feed_add(sec_feed_layer)
8232
+ self.model3.feed_add(first_feed_layer)
8233
+ self.model3.feed_add(sec_feed_layer)
8170
8234
 
8171
8235
 
8172
8236
  def automatic_parameterization(self, input_size, num_classes):
@@ -8630,7 +8694,6 @@ class IntegratedPipeline:
8630
8694
  x = np.array(x)
8631
8695
  x = x.reshape(x.shape[0], -1) # Flatten if necessary
8632
8696
 
8633
-
8634
8697
  try:
8635
8698
  try:
8636
8699
  grads = np.gradient(x)
@@ -8965,14 +9028,7 @@ class IntegratedPipeline:
8965
9028
  print(f'[!] Cannot convert to array: {type(value)}')
8966
9029
  return None
8967
9030
 
8968
-
8969
- def _get_num_classes(self) -> int:
8970
- # Get number of classes from model first
8971
- if hasattr(self, 'model2') and self.model2:
8972
- return self.model2.output.shape[1]
8973
- elif hasattr(self, 'mlp') and self.mlp.layers:
8974
- return self.mlp.layers[-1].b.shape[1]
8975
- return None
9031
+
8976
9032
 
8977
9033
 
8978
9034
  def model_probability_gate(self, x, x2):
@@ -9113,6 +9169,19 @@ class IntegratedPipeline:
9113
9169
 
9114
9170
  return batch_probs
9115
9171
 
9172
+ def _coerce_batch_size(self, batch_size, default=32):
9173
+ """Single, clear path for batch_size type coercion."""
9174
+ try:
9175
+ arr = np.asarray(batch_size)
9176
+ if arr.ndim == 0:
9177
+ return int(arr)
9178
+ elif arr.size > 0:
9179
+ return int(arr.flat[0])
9180
+ else:
9181
+ return default
9182
+ except (TypeError, ValueError):
9183
+ return default
9184
+
9116
9185
  def _batch_prediction_core(self, batch_input_ids: np.ndarray, batch_X: np.ndarray,
9117
9186
  batch_size: Any = None, show_progress: bool = True) -> np.ndarray:
9118
9187
  """
@@ -9132,13 +9201,15 @@ class IntegratedPipeline:
9132
9201
  except:
9133
9202
  batch_size = int(batch_size[0][0])
9134
9203
  except:
9135
- batch_size = int(batch_size)
9204
+ # robust single clear path when both batch size try blocks fails
9205
+ batch_size = self._coerce_batch_size(batch_size, default=32)
9206
+
9136
9207
  else:
9137
9208
  if isinstance(batch_size, (tuple, list, np.ndarray)):
9138
9209
  batch_size = batch_size[0]
9139
9210
  batch_size = len(batch_size)
9140
9211
  else:
9141
- batch_size = int(batch_size)
9212
+ batch_size = batch_size = self._coerce_batch_size(batch_size, default=32)
9142
9213
 
9143
9214
  n_samples = len(batch_input_ids)
9144
9215
  chunks = []
@@ -9155,8 +9226,8 @@ class IntegratedPipeline:
9155
9226
  chunks.append(chunk)
9156
9227
 
9157
9228
  # ✅ Determine number of classes dynamically from first successful chunk
9158
- num_classes = None
9159
- batch_probs = None
9229
+ num_classes = self._get_num_classes()
9230
+ batch_probs = np.zeros((n_samples, num_classes))
9160
9231
 
9161
9232
  for chunk_idx, (chunk_ids, chunk_X) in enumerate(chunks):
9162
9233
  if show_progress:
@@ -9174,19 +9245,16 @@ class IntegratedPipeline:
9174
9245
 
9175
9246
  # ✅ Determine number of classes from first successful chunk
9176
9247
  if num_classes is None:
9177
- if chunk_probs.ndim == 1:
9178
- num_classes = 1
9179
- else:
9180
- num_classes = chunk_probs.shape[1] if chunk_probs.ndim > 1 else len(chunk_probs)
9181
-
9248
+ num_classes = chunk_probs.shape[1] if chunk_probs.ndim > 1 else 1
9249
+
9182
9250
  # Initialize results array
9183
- batch_probs = np.zeros((n_samples, num_classes))
9184
9251
  print(f'\n[=] Detected {num_classes} classes from chunk {chunk_idx + 1}')
9185
9252
 
9186
9253
  # ✅ Handle dimension mismatches
9187
9254
  if chunk_probs.ndim == 1:
9188
9255
  chunk_probs = chunk_probs.reshape(-1, 1)
9189
-
9256
+
9257
+
9190
9258
  # ✅ If chunk has different number of classes, pad or trim
9191
9259
  if chunk_probs.shape[1] != num_classes:
9192
9260
  print(f'[=] Shape mismatch: chunk has {chunk_probs.shape[1]} classes, expected {num_classes}')
@@ -9227,51 +9295,71 @@ class IntegratedPipeline:
9227
9295
 
9228
9296
  return batch_probs if batch_probs is not None else np.array([])
9229
9297
 
9230
- def _process_batch_chunk(self, chunk_ids: np.ndarray, chunk_X: np.ndarray) -> np.ndarray:
9298
+ def _process_batch_chunk(self, chunk_ids: np.ndarray, chunk_X: np.ndarray) -> Any:
9231
9299
  """
9232
9300
  Process a single chunk - core batch logic with memory gate.
9233
9301
  """
9234
-
9235
9302
  chunk_probs = self._batch_model_memory_gate(chunk_ids, chunk_X)
9236
-
9303
+
9304
+ num_classes = self._get_num_classes()
9305
+ if num_classes is None:
9306
+ print('[⚠️] num_classes unavailable in _process_batch_chunk — '
9307
+ 'cannot safely process this chunk')
9308
+ return np.zeros((len(chunk_ids), 1)) # minimal safe fallback shape
9309
+
9237
9310
  needs_fresh = [i for i, p in enumerate(chunk_probs) if p is None]
9238
-
9311
+
9239
9312
  if needs_fresh:
9240
- # Extract samples that need fresh prediction
9241
9313
  fresh_ids = chunk_ids[needs_fresh]
9242
- fresh_X = chunk_X[needs_fresh]
9243
-
9244
- # Get fresh predictions from ensemble
9314
+ fresh_X = chunk_X[needs_fresh]
9315
+
9245
9316
  fresh_probs, _ = self.ensemble.predict_ensemble(
9246
- fresh_ids, fresh_X,
9247
- np.zeros((len(fresh_ids), self._get_num_classes())),
9317
+ fresh_ids, fresh_X,
9318
+ np.zeros((len(fresh_ids), num_classes)),
9248
9319
  method='dynamic', embedded=False
9249
9320
  )
9250
-
9251
- # Store fresh predictions
9321
+
9322
+ # validate fresh_probs shape before assignment
9323
+ fresh_probs = np.asarray(fresh_probs)
9324
+ if fresh_probs.ndim == 1:
9325
+ fresh_probs = fresh_probs[np.newaxis, :]
9326
+
9327
+ cached_count = 0 # track global cache count
9328
+
9252
9329
  for i, fresh_idx in enumerate(needs_fresh):
9253
- chunk_probs[fresh_idx] = fresh_probs[i]
9254
-
9255
- # Cache to memory (first 2 only to avoid spam)
9256
- if fresh_idx < 2:
9330
+ row = fresh_probs[i]
9331
+
9332
+ # guard ensure row matches expected width before storing
9333
+ if row.shape[0] != num_classes:
9334
+ aligned = np.zeros(num_classes)
9335
+ min_len = min(row.shape[0], num_classes)
9336
+ aligned[:min_len] = row[:min_len]
9337
+ row = aligned
9338
+
9339
+ chunk_probs[fresh_idx] = row
9340
+
9341
+ # cap caching meaningfully
9342
+ if cached_count < 2:
9257
9343
  self.modular_prediction_saving(
9258
9344
  fresh_ids[i:i+1],
9259
9345
  fresh_X[i:i+1],
9260
- fresh_probs[i:i+1]
9346
+ row[np.newaxis, :]
9261
9347
  )
9262
-
9263
- # Convert list to array
9264
- return np.array([p if p is not None else np.zeros(self._get_num_classes())
9265
- for p in chunk_probs])
9348
+ cached_count += 1
9349
+
9350
+
9351
+ return np.array([
9352
+ p if p is not None else np.zeros(num_classes)
9353
+ for p in chunk_probs
9354
+ ])
9266
9355
 
9267
9356
 
9268
9357
 
9269
- def _calculate_optimal_batch_size(self, batch_input_ids: np.ndarray, batch_X: Any=None) -> int:
9358
+ def _calculate_optimal_batch_size(self, batch_input_ids: np.ndarray, batch_X: Any=None) -> Any:
9270
9359
  """
9271
9360
  Calculate optimal batch size based on available memory.
9272
9361
  """
9273
9362
  try:
9274
- import psutil
9275
9363
  # Estimate memory per sample
9276
9364
  sample_size = batch_input_ids[0].nbytes + batch_X[0].nbytes if hasattr(batch_X, '__len__') else 1024
9277
9365
  available_memory = psutil.virtual_memory().available
@@ -9281,35 +9369,47 @@ class IntegratedPipeline:
9281
9369
  # Fallback to conservative batch size
9282
9370
  return 32
9283
9371
 
9284
- def _get_num_classes(self) -> int:
9285
- if hasattr(self, 'model2') and self.model2 is not None:
9286
- if hasattr(self.model2, 'output'):
9287
- return self.model2.output.shape[1]
9288
-
9289
- if hasattr(self, 'mlp') and self.mlp is not None:
9290
- if hasattr(self.mlp, 'layers') and len(self.mlp.layers) > 0:
9291
- last_layer = self.mlp.layers[-1]
9292
- if hasattr(last_layer, 'b'):
9293
- return last_layer.b.shape[1]
9294
-
9295
- if hasattr(self, 'reverse_map') and self.reverse_map:
9296
- return len(self.reverse_map)
9297
-
9298
- if hasattr(self, 'label_map') and self.label_map:
9299
- return len(self.label_map)
9300
-
9301
- # infer from any cached probability
9302
- if isinstance(self.memory, dict):
9303
- for value in self.memory.values():
9304
- if isinstance(value, np.ndarray) and value.ndim == 1:
9305
- return value.shape[0]
9306
- if isinstance(value, list) and len(value) > 0:
9307
- if all(isinstance(x, (int, float)) for x in value[:5]):
9308
- return len(value)
9372
+ def _get_num_classes(self, label_map: dict = None, mlp_probs: np.ndarray = None) -> Any:
9373
+ """
9374
+ Single source of truth for num_classes across the entire pipeline.
9375
+
9376
+ Resolution priority:
9377
+ 1. Actual model output shape (model2.output or mlp final layer)
9378
+ this is authoritative since argmax indices are bounded by this
9379
+ 2. label_map length — used only as cross-check / fallback
9380
+ 3. mlp_probs.shape[1] — used only as last-resort fallback
9381
+
9382
+ Logs a warning if sources disagree, since that disagreement
9383
+ is what causes invalid prediction indices downstream.
9384
+ """
9385
+ model_classes = None
9386
+
9387
+ # primary source — actual model output dimension
9388
+ if hasattr(self, 'model2') and self.model2:
9389
+ model_classes = self.model2.output.shape[1]
9390
+ elif hasattr(self, 'mlp') and self.mlp.layers:
9391
+ model_classes = self.mlp.layers[-1].b.shape[1]
9392
+
9393
+ # cross-check against label_map if provided
9394
+ if label_map is not None:
9395
+ label_classes = len(label_map)
9396
+ if model_classes is not None and label_classes != model_classes:
9397
+ print(f'[⚠️] num_classes mismatch: model={model_classes} '
9398
+ f'label_map={label_classes} — using model output as source of truth')
9399
+ elif model_classes is None:
9400
+ model_classes = label_classes
9401
+
9402
+ # cross-check against mlp_probs if provided
9403
+ if mlp_probs is not None:
9404
+ probs_classes = mlp_probs.shape[1] if mlp_probs.ndim > 1 else len(mlp_probs[0])
9405
+ if model_classes is not None and probs_classes != model_classes:
9406
+ print(f'[⚠️] num_classes mismatch: model={model_classes} '
9407
+ f'mlp_probs={probs_classes} — using model output as source of truth')
9408
+ elif model_classes is None:
9409
+ model_classes = probs_classes
9410
+
9411
+ return model_classes
9309
9412
 
9310
- print('[!] Could not determine num_classes, defaulting to 0')
9311
- return 0
9312
-
9313
9413
 
9314
9414
  def predict_async(self, text, callback=None):
9315
9415
  try:
@@ -10196,7 +10296,7 @@ class IntegratedPipeline:
10196
10296
  if not self.model2:
10197
10297
  intents = [d[1] for d in datasets]
10198
10298
  intent_to_id = {intent:i for i, intent in enumerate(sorted(set(intents)))}
10199
- num_classes = len(intent_to_id)
10299
+ num_classes = self._get_num_classes()
10200
10300
  self.model2 = Transformer(
10201
10301
  vocab_size=len(self.vocab),
10202
10302
  d_model=32,
@@ -12532,7 +12632,7 @@ class PipelinePredictionManager:
12532
12632
  self.pipeline.labels = label_map
12533
12633
 
12534
12634
  reverse_map = {v: k for k, v in label_map.items()}
12535
- num_classes = len(label_map)
12635
+ num_classes = self.pipeline._get_num_classes(label_map=label_map)
12536
12636
 
12537
12637
  if X is None and y is None or X is None or y is None:
12538
12638
  print('[🔄] Creating automatic X samples because X is not provided manually.')
@@ -12588,7 +12688,7 @@ class PipelinePredictionManager:
12588
12688
  # Validate all MLP predictions at once
12589
12689
  mlp_pred_indices = np.argmax(mlp_probs, axis=1)
12590
12690
  if num_classes <= 0:
12591
- num_classes = mlp_probs.shape[1] if mlp_probs.ndim > 1 else len(mlp_probs)
12691
+ num_classes = self.pipeline._get_num_classes(mlp_probs=mlp_probs)
12592
12692
 
12593
12693
  valid_mask = mlp_pred_indices < num_classes
12594
12694
  if not np.all(valid_mask):
@@ -12776,8 +12876,13 @@ class PipelinePredictionManager:
12776
12876
  verbose = False
12777
12877
  if float(results[0]['confidence']) < self.pipeline.confidence_threshold:
12778
12878
  verbose = True
12779
-
12780
- self.display_hybrid_results(results, top_k, verbose=verbose)
12879
+
12880
+ payload = {
12881
+ 'X_samples': X,
12882
+ 'input_ids': input_ids
12883
+ }
12884
+
12885
+ self.display_hybrid_results(payload, results, top_k, verbose=verbose)
12781
12886
 
12782
12887
 
12783
12888
  # Use results directly - they already contain calibrated predictions
@@ -13052,7 +13157,7 @@ class PipelinePredictionManager:
13052
13157
  print("\n[🚀] Starting Advanced Hybrid Prediction Method")
13053
13158
 
13054
13159
  reverse_map = {v: k for k, v in label_map.items()}
13055
- num_classes = len(label_map)
13160
+ num_classes = self.pipeline._get_num_classes(label_map=label_map)
13056
13161
 
13057
13162
  self.pipeline.titles = titles
13058
13163
  self.pipeline.labels = label_map
@@ -13118,7 +13223,7 @@ class PipelinePredictionManager:
13118
13223
  # Validate all MLP predictions at once
13119
13224
  mlp_pred_indices = np.argmax(mlp_probs, axis=1)
13120
13225
  if num_classes <= 0:
13121
- num_classes = mlp_probs.shape[1] if mlp_probs.ndim > 1 else len(mlp_probs[0])
13226
+ num_classes = self.pipeline._get_num_classes(mlp_probs=mlp_probs)
13122
13227
 
13123
13228
  valid_mask = mlp_pred_indices < num_classes
13124
13229
  if not np.all(valid_mask):
@@ -13191,10 +13296,23 @@ class PipelinePredictionManager:
13191
13296
  expected_label = None
13192
13297
 
13193
13298
  # MLP prediction
13194
- mlp_class_idx = mlp_pred_indices[i]
13195
- mlp_class_idx = min(mlp_class_idx, num_classes - 1) # Clamped to valid range
13196
- if mlp_class_idx < 0 or mlp_class_idx >= num_classes:
13197
- mlp_class_idx = 0 # Safe default
13299
+ mlp_class_idx = int(mlp_pred_indices[i])
13300
+ is_valid_index = 0 <= mlp_class_idx < num_classes
13301
+ if not is_valid_index:
13302
+ print(f'[⚠️] Invalid mlp_class_idx={mlp_class_idx} for sample {i} '
13303
+ f'(num_classes={num_classes}, title="{display_title}") '
13304
+ f'— marking as low-confidence unknown, NOT defaulting to class 0')
13305
+
13306
+ results.append({
13307
+ 'title' : display_title,
13308
+ 'expected' : expected_label,
13309
+ 'predicted' : None, # explicit unknown, not a fake class
13310
+ 'confidence' : 0.0,
13311
+ 'mlp_class' : None,
13312
+ 'is_valid' : False,
13313
+ 'error' : f'class_index_out_of_range(idx={mlp_class_idx}, num_classes={num_classes})'
13314
+ })
13315
+ continue
13198
13316
 
13199
13317
  mlp_confidence = mlp_probs[i][mlp_class_idx]
13200
13318
  mlp_label = reverse_map.get(mlp_class_idx, f"unknown_{mlp_class_idx}")