predikit 0.4.6__tar.gz → 0.5.0__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 (51) hide show
  1. {predikit-0.4.6 → predikit-0.5.0}/CHANGELOG.md +1 -1
  2. {predikit-0.4.6 → predikit-0.5.0}/PKG-INFO +1 -1
  3. {predikit-0.4.6 → predikit-0.5.0}/pyproject.toml +1 -1
  4. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/__init__.py +1 -1
  5. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/loaders/snowflake.py +40 -19
  6. {predikit-0.4.6 → predikit-0.5.0}/tests/test_loaders_snowflake.py +62 -22
  7. {predikit-0.4.6 → predikit-0.5.0}/uv.lock +1 -1
  8. {predikit-0.4.6 → predikit-0.5.0}/.claude/settings.local.json +0 -0
  9. {predikit-0.4.6 → predikit-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  10. {predikit-0.4.6 → predikit-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  11. {predikit-0.4.6 → predikit-0.5.0}/.github/workflows/publish.yml +0 -0
  12. {predikit-0.4.6 → predikit-0.5.0}/.github/workflows/test.yml +0 -0
  13. {predikit-0.4.6 → predikit-0.5.0}/.gitignore +0 -0
  14. {predikit-0.4.6 → predikit-0.5.0}/.pre-commit-config.yaml +0 -0
  15. {predikit-0.4.6 → predikit-0.5.0}/CLAUDE.md +0 -0
  16. {predikit-0.4.6 → predikit-0.5.0}/CONTRIBUTING.md +0 -0
  17. {predikit-0.4.6 → predikit-0.5.0}/LICENSE +0 -0
  18. {predikit-0.4.6 → predikit-0.5.0}/README.md +0 -0
  19. {predikit-0.4.6 → predikit-0.5.0}/docs/logo.gif +0 -0
  20. {predikit-0.4.6 → predikit-0.5.0}/docs/logo.png +0 -0
  21. {predikit-0.4.6 → predikit-0.5.0}/examples/01_basic_sklearn.py +0 -0
  22. {predikit-0.4.6 → predikit-0.5.0}/examples/02_xgboost_regression.py +0 -0
  23. {predikit-0.4.6 → predikit-0.5.0}/examples/03_orlando_real_estate.py +0 -0
  24. {predikit-0.4.6 → predikit-0.5.0}/examples/04_confidence_routing.py +0 -0
  25. {predikit-0.4.6 → predikit-0.5.0}/examples/05_multi_model_ensemble.py +0 -0
  26. {predikit-0.4.6 → predikit-0.5.0}/examples/06_mlflow_loader.py +0 -0
  27. {predikit-0.4.6 → predikit-0.5.0}/examples/07_snowflake_loader.py +0 -0
  28. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/cli.py +0 -0
  29. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/coerce.py +0 -0
  30. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/ensemble.py +0 -0
  31. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/exceptions.py +0 -0
  32. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/exporters/__init__.py +0 -0
  33. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/exporters/langchain.py +0 -0
  34. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/exporters/openai.py +0 -0
  35. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/introspect.py +0 -0
  36. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/loaders/__init__.py +0 -0
  37. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/loaders/mlflow.py +0 -0
  38. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/registry.py +0 -0
  39. {predikit-0.4.6 → predikit-0.5.0}/src/predikit/tool.py +0 -0
  40. {predikit-0.4.6 → predikit-0.5.0}/tests/__init__.py +0 -0
  41. {predikit-0.4.6 → predikit-0.5.0}/tests/test_cli.py +0 -0
  42. {predikit-0.4.6 → predikit-0.5.0}/tests/test_coerce.py +0 -0
  43. {predikit-0.4.6 → predikit-0.5.0}/tests/test_confidence.py +0 -0
  44. {predikit-0.4.6 → predikit-0.5.0}/tests/test_ensemble.py +0 -0
  45. {predikit-0.4.6 → predikit-0.5.0}/tests/test_exporters_openai.py +0 -0
  46. {predikit-0.4.6 → predikit-0.5.0}/tests/test_introspect.py +0 -0
  47. {predikit-0.4.6 → predikit-0.5.0}/tests/test_loaders_mlflow.py +0 -0
  48. {predikit-0.4.6 → predikit-0.5.0}/tests/test_logging.py +0 -0
  49. {predikit-0.4.6 → predikit-0.5.0}/tests/test_registry.py +0 -0
  50. {predikit-0.4.6 → predikit-0.5.0}/tests/test_tool.py +0 -0
  51. {predikit-0.4.6 → predikit-0.5.0}/tests/test_weighted_ensemble.py +0 -0
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [0.4.6] - 2026-07-01
9
+ ## [0.5.0] - 2026-07-01
10
10
 
11
11
  ### Fixed
12
12
  - Improved `ModelTool` input coercion for optional scalar annotations like `float | None`, `bool | None`, `int | None`, and `str | None`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: predikit
3
- Version: 0.4.6
3
+ Version: 0.5.0
4
4
  Summary: Wrap any trained sklearn/XGBoost model as an LLM-callable tool: auto-generated JSON schemas, Pydantic I/O, OpenAI & LangChain ready, zero boilerplate.
5
5
  Project-URL: Homepage, https://github.com/Tejas-TA/predikit
6
6
  Project-URL: Repository, https://github.com/Tejas-TA/predikit
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "predikit"
7
- version = "0.4.6"
7
+ version = "0.5.0"
8
8
  description = "Wrap any trained sklearn/XGBoost model as an LLM-callable tool: auto-generated JSON schemas, Pydantic I/O, OpenAI & LangChain ready, zero boilerplate."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -4,4 +4,4 @@ from .registry import ToolRegistry
4
4
  from .tool import ModelTool
5
5
 
6
6
  __all__ = ["ModelTool", "ToolRegistry", "ModelEnsemble", "LowConfidenceError"]
7
- __version__ = "0.4.6"
7
+ __version__ = "0.5.0"
@@ -13,26 +13,47 @@ except ImportError:
13
13
  Registry = None # type: ignore[assignment,misc]
14
14
 
15
15
 
16
- class _SnowflakeShim:
17
- """Wraps a Snowflake Model Registry model to look like an sklearn estimator."""
16
+ class _SnowflakeShim:
17
+ """Wraps a Snowflake Model Registry model to look like an sklearn estimator."""
18
18
 
19
- def __init__(
20
- self, sf_model: Any, output_method: str = "predict", classes: list | None = None
21
- ) -> None:
22
- if not callable(getattr(sf_model, output_method, None)):
23
- raise AttributeError(
24
- f"Snowflake model object has no callable method {output_method!r}."
25
- )
26
- self._model = sf_model
27
- self._method = output_method
28
- if classes is not None:
29
- self.classes_ = classes
30
-
31
- def predict(self, X: Any) -> np.ndarray:
32
- result = getattr(self._model, self._method)(X)
33
- if hasattr(result, "to_numpy"):
34
- return np.asarray(result.to_numpy()).flatten()
35
- return np.asarray(result).flatten()
19
+ def __init__(
20
+ self, sf_model: Any, output_method: str = "predict", classes: list | None = None
21
+ ) -> None:
22
+ if not callable(getattr(sf_model, output_method, None)):
23
+ raise AttributeError(
24
+ f"Snowflake model object has no callable method {output_method!r}."
25
+ )
26
+ self._model = sf_model
27
+ self._method = output_method
28
+ underlying = getattr(sf_model, "_model_impl", None)
29
+ if underlying is not None:
30
+ underlying = getattr(underlying, "sklearn_model", None)
31
+
32
+ if classes is None and underlying is not None and hasattr(underlying, "classes_"):
33
+ classes = list(underlying.classes_)
34
+
35
+ if classes is not None:
36
+ self.classes_ = classes
37
+
38
+ if underlying is not None:
39
+ if hasattr(underlying, "feature_names_in_"):
40
+ self.feature_names_in_ = underlying.feature_names_in_
41
+ if hasattr(underlying, "n_features_in_"):
42
+ self.n_features_in_ = underlying.n_features_in_
43
+
44
+ def predict(self, X: Any) -> np.ndarray:
45
+ result = getattr(self._model, self._method)(X)
46
+ if hasattr(result, "to_numpy"):
47
+ return np.asarray(result.to_numpy()).flatten()
48
+ return np.asarray(result).flatten()
49
+
50
+ def predict_proba(self, X: Any) -> np.ndarray:
51
+ if callable(getattr(self._model, "predict_proba", None)):
52
+ return np.asarray(self._model.predict_proba(X))
53
+ raise NotImplementedError(
54
+ "predict_proba is not available for this Snowflake model object. "
55
+ "The underlying model may not expose it."
56
+ )
36
57
 
37
58
 
38
59
  def from_snowflake(
@@ -24,7 +24,7 @@ class MemberInput(BaseModel):
24
24
  # ---------------------------------------------------------------------------
25
25
 
26
26
 
27
- class TestSnowflakeShim:
27
+ class TestSnowflakeShim:
28
28
  def test_predict_calls_correct_method(self):
29
29
  mock_model = MagicMock()
30
30
  mock_model.predict.return_value = np.array([1])
@@ -41,12 +41,12 @@ class TestSnowflakeShim:
41
41
  mock_model.predict_proba.assert_called_once()
42
42
  assert result[0] == pytest.approx(0.27, abs=1e-6)
43
43
 
44
- def test_classes_optional(self):
45
- shim_no_classes = _SnowflakeShim(MagicMock())
46
- assert not hasattr(shim_no_classes, "classes_")
47
-
48
- shim_with_classes = _SnowflakeShim(MagicMock(), classes=[0, 1])
49
- assert shim_with_classes.classes_ == [0, 1]
44
+ def test_classes_optional(self):
45
+ shim_no_classes = _SnowflakeShim(MagicMock(spec=["predict"]))
46
+ assert not hasattr(shim_no_classes, "classes_")
47
+
48
+ shim_with_classes = _SnowflakeShim(MagicMock(spec=["predict"]), classes=[0, 1])
49
+ assert shim_with_classes.classes_ == [0, 1]
50
50
 
51
51
  def test_predict_flattens_dataframe_result(self):
52
52
  import pandas as pd
@@ -58,13 +58,24 @@ class TestSnowflakeShim:
58
58
  assert result.ndim == 1
59
59
  assert result[0] == 1
60
60
 
61
- def test_predict_flattens_numpy_result(self):
62
- mock_model = MagicMock()
63
- mock_model.predict.return_value = np.array([[0.73]])
64
- shim = _SnowflakeShim(mock_model)
65
- result = shim.predict(np.array([[1.0]]))
66
- assert result.ndim == 1
67
- assert result[0] == pytest.approx(0.73)
61
+ def test_predict_flattens_numpy_result(self):
62
+ mock_model = MagicMock()
63
+ mock_model.predict.return_value = np.array([[0.73]])
64
+ shim = _SnowflakeShim(mock_model)
65
+ result = shim.predict(np.array([[1.0]]))
66
+ assert result.ndim == 1
67
+ assert result[0] == pytest.approx(0.73)
68
+
69
+ def test_predict_proba_delegates_to_underlying_model(self):
70
+ mock_model = MagicMock()
71
+ mock_model.predict_proba.return_value = np.array([[0.2, 0.8]])
72
+ shim = _SnowflakeShim(mock_model)
73
+
74
+ result = shim.predict_proba(np.array([[1.0, 2.0, 3.0]]))
75
+
76
+ mock_model.predict_proba.assert_called_once()
77
+ assert result.shape == (1, 2)
78
+ assert result[0, 1] == pytest.approx(0.8)
68
79
 
69
80
 
70
81
  # ---------------------------------------------------------------------------
@@ -80,7 +91,7 @@ def _make_mock_session_and_model(return_value):
80
91
  return mock_session, mock_sf_model
81
92
 
82
93
 
83
- class TestFromSnowflake:
94
+ class TestFromSnowflake:
84
95
  def test_roundtrip_invoke(self):
85
96
  mock_session, mock_sf_model = _make_mock_session_and_model(np.array([1]))
86
97
 
@@ -123,10 +134,10 @@ class TestFromSnowflake:
123
134
  mock_registry.get_model.assert_called_once_with("VACATION_CHURN")
124
135
  mock_registry.get_model.return_value.version.assert_called_once_with("V3")
125
136
 
126
- def test_custom_output_method_forwarded(self):
127
- mock_session = MagicMock()
128
- mock_sf_model = MagicMock()
129
- mock_sf_model.score.return_value = np.array([0.87])
137
+ def test_custom_output_method_forwarded(self):
138
+ mock_session = MagicMock()
139
+ mock_sf_model = MagicMock()
140
+ mock_sf_model.score.return_value = np.array([0.87])
130
141
 
131
142
  with patch("predikit.loaders.snowflake.Registry") as MockRegistry:
132
143
  MockRegistry.return_value.get_model.return_value.version.return_value = mock_sf_model
@@ -143,9 +154,38 @@ class TestFromSnowflake:
143
154
  output_method="score",
144
155
  )
145
156
 
146
- result = tool.invoke({"tenure_months": 12.0, "trips_last_year": 5.0, "avg_spend": 300.0})
147
- assert result["score"] == pytest.approx(0.87, abs=1e-6)
148
- mock_sf_model.score.assert_called_once()
157
+ result = tool.invoke({"tenure_months": 12.0, "trips_last_year": 5.0, "avg_spend": 300.0})
158
+ assert result["score"] == pytest.approx(0.87, abs=1e-6)
159
+ mock_sf_model.score.assert_called_once()
160
+
161
+ def test_confidence_routing_uses_predict_proba(self):
162
+ mock_session = MagicMock()
163
+ mock_sf_model = MagicMock()
164
+ mock_sf_model.predict.return_value = np.array([1])
165
+ mock_sf_model.predict_proba.return_value = np.array([[0.6, 0.4]])
166
+ mock_sf_model.classes_ = [0, 1]
167
+
168
+ with patch("predikit.loaders.snowflake.Registry") as MockRegistry:
169
+ MockRegistry.return_value.get_model.return_value.version.return_value = mock_sf_model
170
+
171
+ tool = from_snowflake(
172
+ session=mock_session,
173
+ model_name="CHURN",
174
+ model_version="V1",
175
+ name="churn",
176
+ description="Churn.",
177
+ input_schema=MemberInput,
178
+ output_name="churn_class",
179
+ output_description="Churn class",
180
+ confidence_threshold=0.9,
181
+ on_low_confidence="warn",
182
+ )
183
+
184
+ result = tool.invoke({"tenure_months": 12.0, "trips_last_year": 5.0, "avg_spend": 300.0})
185
+ assert result["churn_class"] == 1
186
+ assert result["_low_confidence"] is True
187
+ assert result["_confidence"] == pytest.approx(0.6)
188
+ mock_sf_model.predict_proba.assert_called_once()
149
189
 
150
190
  def test_model_tool_kwargs_forwarded(self):
151
191
  mock_session, mock_sf_model = _make_mock_session_and_model(np.array([1]))
@@ -560,7 +560,7 @@ wheels = [
560
560
 
561
561
  [[package]]
562
562
  name = "colorama"
563
- version = "0.4.6"
563
+ version = "0.5.0"
564
564
  source = { registry = "https://pypi.org/simple" }
565
565
  sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
566
566
  wheels = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes