Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 61 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.1.0] - 2026-07-30

Ports integration fixes and compatibility improvements developed in the
`whitebox-xai-azure` monorepo's `sdk/` directory since the 1.0.0 cut.

### Fixed
- **XGBoost/LightGBM**: `XGBoostMonitor.predict()`/`LightGBMMonitor.predict()`
and `wrap_xgboost_model()`/`wrap_lightgbm_model()` called a nonexistent
`log_predictions()` method (raising `AttributeError`) instead of
`ModelMonitor.log_batch()` — the primary documented usage of both
integrations was broken.
- **TensorFlow/Keras**: `KerasMonitor.predict()` called `log_prediction()`/
`log_batch()` with invalid keyword arguments (`prediction=`, `actual=`,
`predictions=`, `actuals=`), raising `TypeError` on every call.
`KerasMonitor.log_epoch()`/`log_checkpoint()` (and the
`WhiteBoxXAICallback` training callback) called a `log_custom_metric()`
method that didn't exist, raising `AttributeError`.
`KerasMonitor.set_baseline()` raised `TypeError` when computing baseline
predictions, since it called the parent `ModelMonitor.set_baseline()`
with two positional arguments against a signature that only accepted one
(see the `ModelMonitor.set_baseline()` fix below).
`register_saved_model()` raised `ValueError` unless `self.model` was set,
even though it exists specifically to register a model saved to disk.
`wrap_keras_model()` also called `log_batch()` with invalid keyword
arguments.
- **Hugging Face Transformers**: `TransformersMonitor.log_prediction_transformers()`/
`log_batch_transformers()` called `log_prediction()`/`log_batch()` with
invalid keyword arguments, raising `TypeError` on every prediction.
`TransformersMonitor.set_baseline()` had the same `ModelMonitor.set_baseline()`
argument-count crash as `KerasMonitor` above.
`wrap_transformers_pipeline()` reassigned `pipeline.__call__` on a pipeline
*instance*, which Python never actually invokes (dunder-method lookup
happens on the type, not the instance) — wrapped pipelines silently never
logged any predictions.
- **LangChain**: `LangChainMonitor.log_chain_execution()`,
`log_agent_execution()`, `log_llm_call()`, `log_tool_call()`, and
`log_rag_retrieval()` all passed `prediction=` to `log_prediction()`,
which only accepts `output=` — all five convenience-logging methods
raised `TypeError`.
- `OfflineQueue.clear_completed()` used an exclusive day-boundary comparison
(`created_at < ...`), so a completed record exactly `older_than_days` old
was never cleared. Now inclusive (`<=`).

### Added
- `whiteboxxai.integrations.langchain`/`langchain_agents` now try
`langchain_core.*` first (the modern, lighter-weight package) and fall
back to the legacy `langchain.callbacks.base`/`langchain.schema.*`
locations, so the SDK works with `langchain-core`-only installs.
`AgentExecutor`/`Chain` (higher-level `langchain`-only abstractions) are
probed independently so their absence no longer disables the rest of the
integration.
- `ModelMonitor.set_baseline()` accepts an optional `labels` argument for
baseline predictions/labels.
- `KerasMonitor.log_custom_metric()`.

## [1.0.0] - 2026-07-14

First stable, production release. This release realigns the SDK with the
Expand Down Expand Up @@ -153,6 +208,9 @@ from the git history between the `0.2.0` and `0.2.1` tags:
- PII detection and masking
- Secure API key handling

[Unreleased]: https://github.com/AgentaFlow/whitebox-python-sdk/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/AgentaFlow/whitebox-python-sdk/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/AgentaFlow/whitebox-python-sdk/releases/tag/v0.1.0
[Unreleased]: https://github.com/AgentaFlow/whitebox-python-sdk/compare/1.1.0...HEAD
[1.1.0]: https://github.com/AgentaFlow/whitebox-python-sdk/compare/1.0.0...1.1.0
[1.0.0]: https://github.com/AgentaFlow/whitebox-python-sdk/compare/0.2.1...1.0.0
[0.2.1]: https://github.com/AgentaFlow/whitebox-python-sdk/compare/0.2.0...0.2.1
[0.2.0]: https://github.com/AgentaFlow/whitebox-python-sdk/compare/0.1.0...0.2.0
[0.1.0]: https://github.com/AgentaFlow/whitebox-python-sdk/releases/tag/0.1.0
2 changes: 1 addition & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ log_batch(predictions: List[dict])
Log multiple predictions.

```python
set_baseline(data: np.ndarray, **kwargs)
set_baseline(data: np.ndarray, labels: Optional[np.ndarray] = None)
```
Set baseline data for drift detection.

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "whitebox-xai-sdk"
version = "1.0.0"
version = "1.1.0"
description = "Python SDK for WhiteBox XAI - AI Observability & Explainability Platform"
readme = "README.md"
authors = [
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from unittest.mock import AsyncMock, Mock, patch

import numpy as np
import pytest

from whiteboxxai.client import WhiteBoxXAI
Expand Down Expand Up @@ -178,6 +179,33 @@ def test_get_drift_reports(self):
assert reports[0]["drift_score"] == 0.8


class TestBaselineData:
"""Tests for ModelMonitor.set_baseline()."""

def test_set_baseline_data_only(self):
"""Test setting baseline data without labels."""
mock_client = Mock(spec=WhiteBoxXAI)
monitor = ModelMonitor(client=mock_client, model_id="model-123")
data = np.array([[1.0, 2.0], [3.0, 4.0]])

monitor.set_baseline(data)

assert monitor._baseline_data is data
assert monitor._baseline_labels is None

def test_set_baseline_with_labels(self):
"""Test setting baseline data with labels."""
mock_client = Mock(spec=WhiteBoxXAI)
monitor = ModelMonitor(client=mock_client, model_id="model-123")
data = np.array([[1.0, 2.0], [3.0, 4.0]])
labels = np.array([0, 1])

monitor.set_baseline(data, labels=labels)

assert monitor._baseline_data is data
assert monitor._baseline_labels is labels


class TestAlertManagement:
"""Tests for alert management."""

Expand Down
45 changes: 45 additions & 0 deletions tests/unit/test_offline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
Tests for SDK Offline Queue Module

Tests for offline mode queueing functionality.
"""

import sqlite3

from whiteboxxai.offline import OfflineQueue, OperationPriority, OperationType


class TestOfflineQueueClearCompleted:
"""Tests for OfflineQueue.clear_completed()."""

def test_clears_records_exactly_older_than_days_old(self, tmp_path):
"""A completed record whose age is exactly `older_than_days` must be
cleared (boundary is inclusive: created_at <= cutoff, not <)."""
db_path = str(tmp_path / "queue.db")
queue = OfflineQueue(db_path=db_path)

op_id = queue.enqueue(OperationType.PREDICT, {"id": 1}, OperationPriority.NORMAL)
queue.mark_success(op_id)

with sqlite3.connect(db_path) as conn:
conn.execute(
"UPDATE queue SET created_at = datetime('now', '-7 days') WHERE id = ?",
(op_id,),
)
conn.commit()

queue.clear_completed(older_than_days=7)

assert queue.get_statistics()["total"] == 0

def test_keeps_records_younger_than_older_than_days(self, tmp_path):
"""A completed record younger than `older_than_days` must be kept."""
db_path = str(tmp_path / "queue.db")
queue = OfflineQueue(db_path=db_path)

op_id = queue.enqueue(OperationType.PREDICT, {"id": 1}, OperationPriority.NORMAL)
queue.mark_success(op_id)

queue.clear_completed(older_than_days=7)

assert queue.get_statistics()["total"] == 1
93 changes: 51 additions & 42 deletions whiteboxxai/integrations/boosting.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,31 @@
lgb = None


def _build_prediction_batch(
inputs: np.ndarray,
predictions: np.ndarray,
actuals: Optional[np.ndarray] = None,
probabilities: Optional[np.ndarray] = None,
) -> List[Dict[str, Any]]:
"""Build per-sample dicts for ModelMonitor.log_batch() from parallel arrays."""

def _item(value: Any) -> Any:
return value.tolist() if isinstance(value, np.ndarray) else value

batch = []
for i in range(len(inputs)):
item: Dict[str, Any] = {
"inputs": _item(inputs[i]),
"output": _item(predictions[i]),
}
if actuals is not None:
item["actual"] = _item(actuals[i])
if probabilities is not None:
item["probability"] = _item(probabilities[i])
batch.append(item)
return batch


class XGBoostMonitor(ModelMonitor):
"""
Monitor for XGBoost models.
Expand Down Expand Up @@ -241,23 +266,13 @@ def predict(

# Log predictions
if log_predictions:
pred_metadata = metadata or {}

# Add feature importance for this batch if enabled
if self.track_feature_importance:
try:
importance = self._get_feature_importance(model)
if importance:
pred_metadata["feature_importance"] = importance
except Exception as e:
warnings.warn(f"Failed to extract feature importance: {e}")

self.log_predictions(
inputs=X,
predictions=predictions,
actuals=y_true,
probabilities=probabilities,
metadata=pred_metadata,
self.log_batch(
_build_prediction_batch(
inputs=X,
predictions=predictions,
actuals=y_true,
probabilities=probabilities,
)
)

return predictions
Expand Down Expand Up @@ -476,23 +491,13 @@ def predict(

# Log predictions
if log_predictions:
pred_metadata = metadata or {}

# Add feature importance for this batch if enabled
if self.track_feature_importance:
try:
importance = self._get_feature_importance(model)
if importance:
pred_metadata["feature_importance"] = importance
except Exception as e:
warnings.warn(f"Failed to extract feature importance: {e}")

self.log_predictions(
inputs=X,
predictions=predictions,
actuals=y_true,
probabilities=probabilities,
metadata=pred_metadata,
self.log_batch(
_build_prediction_batch(
inputs=X,
predictions=predictions,
actuals=y_true,
probabilities=probabilities,
)
)

return predictions
Expand Down Expand Up @@ -558,7 +563,7 @@ def wrap_xgboost_model(model: Any, monitor: XGBoostMonitor, auto_register: bool
>>> wrapped_model = wrap_xgboost_model(model, monitor)
>>> predictions = wrapped_model.predict(X_test) # Auto-logged
"""
if auto_register and not monitor._model_id:
if auto_register and not monitor.model_id:
monitor.register_from_model(model)

# Store original methods
Expand All @@ -574,7 +579,7 @@ def wrapped_predict(X, *args, **kwargs):

# Log predictions
try:
monitor.log_predictions(inputs=X, predictions=predictions)
monitor.log_batch(_build_prediction_batch(inputs=X, predictions=predictions))
except Exception as e:
warnings.warn(f"Failed to log predictions: {e}")

Expand All @@ -589,8 +594,10 @@ def wrapped_predict_proba(X, *args, **kwargs):
# Log with probabilities
try:
predictions = np.argmax(probabilities, axis=1)
monitor.log_predictions(
inputs=X, predictions=predictions, probabilities=probabilities
monitor.log_batch(
_build_prediction_batch(
inputs=X, predictions=predictions, probabilities=probabilities
)
)
except Exception as e:
warnings.warn(f"Failed to log predictions: {e}")
Expand Down Expand Up @@ -624,7 +631,7 @@ def wrap_lightgbm_model(model: Any, monitor: LightGBMMonitor, auto_register: boo
>>> wrapped_model = wrap_lightgbm_model(model, monitor)
>>> predictions = wrapped_model.predict(X_test) # Auto-logged
"""
if auto_register and not monitor._model_id:
if auto_register and not monitor.model_id:
monitor.register_from_model(model)

# Store original methods
Expand All @@ -640,7 +647,7 @@ def wrapped_predict(X, *args, **kwargs):

# Log predictions
try:
monitor.log_predictions(inputs=X, predictions=predictions)
monitor.log_batch(_build_prediction_batch(inputs=X, predictions=predictions))
except Exception as e:
warnings.warn(f"Failed to log predictions: {e}")

Expand All @@ -655,8 +662,10 @@ def wrapped_predict_proba(X, *args, **kwargs):
# Log with probabilities
try:
predictions = np.argmax(probabilities, axis=1)
monitor.log_predictions(
inputs=X, predictions=predictions, probabilities=probabilities
monitor.log_batch(
_build_prediction_batch(
inputs=X, predictions=predictions, probabilities=probabilities
)
)
except Exception as e:
warnings.warn(f"Failed to log predictions: {e}")
Expand Down
Loading
Loading