predikit 0.4.5__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 (121) hide show
  1. {predikit-0.4.5 → predikit-0.5.0}/.gitignore +6 -5
  2. {predikit-0.4.5 → predikit-0.5.0}/CHANGELOG.md +10 -0
  3. {predikit-0.4.5 → predikit-0.5.0}/PKG-INFO +2 -2
  4. {predikit-0.4.5 → predikit-0.5.0}/pyproject.toml +2 -2
  5. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/__init__.py +1 -1
  6. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/cli.py +2 -2
  7. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/loaders/snowflake.py +40 -19
  8. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/tool.py +18 -3
  9. {predikit-0.4.5 → predikit-0.5.0}/tests/test_cli.py +1 -1
  10. {predikit-0.4.5 → predikit-0.5.0}/tests/test_ensemble.py +1 -1
  11. {predikit-0.4.5 → predikit-0.5.0}/tests/test_loaders_snowflake.py +62 -22
  12. {predikit-0.4.5 → predikit-0.5.0}/tests/test_tool.py +21 -0
  13. {predikit-0.4.5 → predikit-0.5.0}/uv.lock +2 -2
  14. predikit-0.4.5/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/MLmodel +0 -23
  15. predikit-0.4.5/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/conda.yaml +0 -22
  16. predikit-0.4.5/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/model.skops +0 -0
  17. predikit-0.4.5/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/python_env.yaml +0 -7
  18. predikit-0.4.5/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/requirements.txt +0 -15
  19. predikit-0.4.5/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/MLmodel +0 -23
  20. predikit-0.4.5/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/conda.yaml +0 -22
  21. predikit-0.4.5/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/model.skops +0 -0
  22. predikit-0.4.5/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/python_env.yaml +0 -7
  23. predikit-0.4.5/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/requirements.txt +0 -15
  24. predikit-0.4.5/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/MLmodel +0 -23
  25. predikit-0.4.5/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/conda.yaml +0 -22
  26. predikit-0.4.5/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/model.skops +0 -0
  27. predikit-0.4.5/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/python_env.yaml +0 -7
  28. predikit-0.4.5/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/requirements.txt +0 -15
  29. predikit-0.4.5/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/MLmodel +0 -23
  30. predikit-0.4.5/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/conda.yaml +0 -22
  31. predikit-0.4.5/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/model.skops +0 -0
  32. predikit-0.4.5/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/python_env.yaml +0 -7
  33. predikit-0.4.5/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/requirements.txt +0 -15
  34. predikit-0.4.5/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/MLmodel +0 -23
  35. predikit-0.4.5/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/conda.yaml +0 -22
  36. predikit-0.4.5/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/model.skops +0 -0
  37. predikit-0.4.5/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/python_env.yaml +0 -7
  38. predikit-0.4.5/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/requirements.txt +0 -15
  39. predikit-0.4.5/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/MLmodel +0 -23
  40. predikit-0.4.5/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/conda.yaml +0 -22
  41. predikit-0.4.5/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/model.skops +0 -0
  42. predikit-0.4.5/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/python_env.yaml +0 -7
  43. predikit-0.4.5/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/requirements.txt +0 -15
  44. predikit-0.4.5/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/MLmodel +0 -23
  45. predikit-0.4.5/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/conda.yaml +0 -22
  46. predikit-0.4.5/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/model.skops +0 -0
  47. predikit-0.4.5/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/python_env.yaml +0 -7
  48. predikit-0.4.5/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/requirements.txt +0 -15
  49. predikit-0.4.5/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/MLmodel +0 -23
  50. predikit-0.4.5/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/conda.yaml +0 -22
  51. predikit-0.4.5/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/model.skops +0 -0
  52. predikit-0.4.5/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/python_env.yaml +0 -7
  53. predikit-0.4.5/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/requirements.txt +0 -15
  54. predikit-0.4.5/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/MLmodel +0 -23
  55. predikit-0.4.5/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/conda.yaml +0 -22
  56. predikit-0.4.5/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/model.skops +0 -0
  57. predikit-0.4.5/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/python_env.yaml +0 -7
  58. predikit-0.4.5/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/requirements.txt +0 -15
  59. predikit-0.4.5/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/MLmodel +0 -23
  60. predikit-0.4.5/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/conda.yaml +0 -22
  61. predikit-0.4.5/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/model.skops +0 -0
  62. predikit-0.4.5/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/python_env.yaml +0 -7
  63. predikit-0.4.5/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/requirements.txt +0 -15
  64. predikit-0.4.5/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/MLmodel +0 -23
  65. predikit-0.4.5/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/conda.yaml +0 -22
  66. predikit-0.4.5/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/model.skops +0 -0
  67. predikit-0.4.5/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/python_env.yaml +0 -7
  68. predikit-0.4.5/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/requirements.txt +0 -15
  69. predikit-0.4.5/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/MLmodel +0 -23
  70. predikit-0.4.5/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/conda.yaml +0 -22
  71. predikit-0.4.5/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/model.skops +0 -0
  72. predikit-0.4.5/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/python_env.yaml +0 -7
  73. predikit-0.4.5/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/requirements.txt +0 -15
  74. predikit-0.4.5/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/MLmodel +0 -23
  75. predikit-0.4.5/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/conda.yaml +0 -22
  76. predikit-0.4.5/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/model.skops +0 -0
  77. predikit-0.4.5/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/python_env.yaml +0 -7
  78. predikit-0.4.5/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/requirements.txt +0 -15
  79. predikit-0.4.5/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/MLmodel +0 -23
  80. predikit-0.4.5/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/conda.yaml +0 -22
  81. predikit-0.4.5/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/model.skops +0 -0
  82. predikit-0.4.5/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/python_env.yaml +0 -7
  83. predikit-0.4.5/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/requirements.txt +0 -15
  84. {predikit-0.4.5 → predikit-0.5.0}/.claude/settings.local.json +0 -0
  85. {predikit-0.4.5 → predikit-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  86. {predikit-0.4.5 → predikit-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  87. {predikit-0.4.5 → predikit-0.5.0}/.github/workflows/publish.yml +0 -0
  88. {predikit-0.4.5 → predikit-0.5.0}/.github/workflows/test.yml +0 -0
  89. {predikit-0.4.5 → predikit-0.5.0}/.pre-commit-config.yaml +0 -0
  90. {predikit-0.4.5 → predikit-0.5.0}/CLAUDE.md +0 -0
  91. {predikit-0.4.5 → predikit-0.5.0}/CONTRIBUTING.md +0 -0
  92. {predikit-0.4.5 → predikit-0.5.0}/LICENSE +0 -0
  93. {predikit-0.4.5 → predikit-0.5.0}/README.md +0 -0
  94. {predikit-0.4.5 → predikit-0.5.0}/docs/logo.gif +0 -0
  95. {predikit-0.4.5 → predikit-0.5.0}/docs/logo.png +0 -0
  96. {predikit-0.4.5 → predikit-0.5.0}/examples/01_basic_sklearn.py +0 -0
  97. {predikit-0.4.5 → predikit-0.5.0}/examples/02_xgboost_regression.py +0 -0
  98. {predikit-0.4.5 → predikit-0.5.0}/examples/03_orlando_real_estate.py +0 -0
  99. {predikit-0.4.5 → predikit-0.5.0}/examples/04_confidence_routing.py +0 -0
  100. {predikit-0.4.5 → predikit-0.5.0}/examples/05_multi_model_ensemble.py +0 -0
  101. {predikit-0.4.5 → predikit-0.5.0}/examples/06_mlflow_loader.py +0 -0
  102. {predikit-0.4.5 → predikit-0.5.0}/examples/07_snowflake_loader.py +0 -0
  103. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/coerce.py +0 -0
  104. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/ensemble.py +0 -0
  105. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/exceptions.py +0 -0
  106. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/exporters/__init__.py +0 -0
  107. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/exporters/langchain.py +0 -0
  108. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/exporters/openai.py +0 -0
  109. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/introspect.py +0 -0
  110. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/loaders/__init__.py +0 -0
  111. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/loaders/mlflow.py +0 -0
  112. {predikit-0.4.5 → predikit-0.5.0}/src/predikit/registry.py +0 -0
  113. {predikit-0.4.5 → predikit-0.5.0}/tests/__init__.py +0 -0
  114. {predikit-0.4.5 → predikit-0.5.0}/tests/test_coerce.py +0 -0
  115. {predikit-0.4.5 → predikit-0.5.0}/tests/test_confidence.py +0 -0
  116. {predikit-0.4.5 → predikit-0.5.0}/tests/test_exporters_openai.py +0 -0
  117. {predikit-0.4.5 → predikit-0.5.0}/tests/test_introspect.py +0 -0
  118. {predikit-0.4.5 → predikit-0.5.0}/tests/test_loaders_mlflow.py +0 -0
  119. {predikit-0.4.5 → predikit-0.5.0}/tests/test_logging.py +0 -0
  120. {predikit-0.4.5 → predikit-0.5.0}/tests/test_registry.py +0 -0
  121. {predikit-0.4.5 → predikit-0.5.0}/tests/test_weighted_ensemble.py +0 -0
@@ -1,13 +1,14 @@
1
1
  __pycache__/
2
2
  *.py[cod]
3
3
  *.egg-info/
4
- dist/
5
- build/
6
- .coverage
7
- coverage.xml
4
+ dist/
5
+ build/
6
+ mlruns/
7
+ .coverage
8
+ coverage.xml
8
9
  htmlcov/
9
10
  .pytest_cache/
10
11
  .venv/
11
12
  venv/
12
13
  *.egg
13
- file.py
14
+ file.py
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-07-01
10
+
11
+ ### Fixed
12
+ - Improved `ModelTool` input coercion for optional scalar annotations like `float | None`, `bool | None`, `int | None`, and `str | None`
13
+ - Added test coverage for optional scalar string inputs to keep LLM-friendly coercion consistent across modern Pydantic schemas
14
+
15
+ ### Changed
16
+ - Removed generated MLflow run artifacts from source control and ignored future `mlruns/` outputs
17
+ - Cleaned package metadata and CLI text to avoid Unicode dash rendering issues in terminals and package indexes
18
+
9
19
  ## [0.4.5] - 2026-07-01
10
20
 
11
21
  ### Fixed
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: predikit
3
- Version: 0.4.5
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.
3
+ Version: 0.5.0
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
7
7
  Project-URL: Documentation, https://github.com/Tejas-TA/predikit#readme
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "predikit"
7
- version = "0.4.5"
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."
7
+ version = "0.5.0"
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"}
11
11
  requires-python = ">=3.10"
@@ -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.5"
7
+ __version__ = "0.5.0"
@@ -16,7 +16,7 @@ if _CLICK_AVAILABLE:
16
16
 
17
17
  @click.group()
18
18
  def cli() -> None:
19
- """predikit ML model utilities for LLM agents."""
19
+ """predikit: ML model utilities for LLM agents."""
20
20
 
21
21
  @cli.command()
22
22
  @click.argument("model_path", type=click.Path(exists=True))
@@ -53,7 +53,7 @@ if _CLICK_AVAILABLE:
53
53
  for fname in meta["feature_names"]:
54
54
  click.echo(f" {fname}")
55
55
  else:
56
- click.echo("Feature names: (none fit the model with a named DataFrame to enable)")
56
+ click.echo("Feature names: (none - fit the model with a named DataFrame to enable)")
57
57
 
58
58
  if meta["classes"] is not None:
59
59
  click.echo(f"Classes: {meta['classes']}")
@@ -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(
@@ -4,7 +4,8 @@ import asyncio
4
4
  import time
5
5
  import warnings
6
6
  from collections.abc import Callable
7
- from typing import Any
7
+ from types import UnionType
8
+ from typing import Any, Union, get_args, get_origin
8
9
 
9
10
  import numpy as np
10
11
  from pydantic import BaseModel
@@ -17,6 +18,7 @@ from .introspect import introspect
17
18
 
18
19
  _VALID_ON_LOW_CONFIDENCE = {"warn", "raise", "fallback"}
19
20
  _SCALAR_TYPES = (bool, int, float, str)
21
+ _NONE_TYPE = type(None)
20
22
 
21
23
 
22
24
  class ModelTool:
@@ -143,9 +145,10 @@ class ModelTool:
143
145
  for k, v in input_dict.items():
144
146
  field = fields.get(k)
145
147
  annotation = getattr(field, "annotation", None) if field else None
146
- if annotation in _SCALAR_TYPES:
148
+ scalar_type = _scalar_type(annotation)
149
+ if scalar_type is not None and v is not None:
147
150
  try:
148
- result[k] = coerce_value(v, annotation)
151
+ result[k] = coerce_value(v, scalar_type)
149
152
  except (ValueError, TypeError):
150
153
  result[k] = v
151
154
  else:
@@ -162,3 +165,15 @@ class ModelTool:
162
165
  except ImportError:
163
166
  pass
164
167
  return np.array([features])
168
+
169
+
170
+ def _scalar_type(annotation: Any) -> type | None:
171
+ if annotation in _SCALAR_TYPES:
172
+ return annotation
173
+
174
+ if get_origin(annotation) in (Union, UnionType):
175
+ args = [arg for arg in get_args(annotation) if arg is not _NONE_TYPE]
176
+ if len(args) == 1:
177
+ return _scalar_type(args[0])
178
+
179
+ return None
@@ -19,7 +19,7 @@ def runner():
19
19
 
20
20
  @pytest.fixture
21
21
  def reg_pkl(tmp_path):
22
- """Regression model fitted with named features has feature_names_in_."""
22
+ """Regression model fitted with named features, so it has feature_names_in_."""
23
23
  X, y = load_iris(return_X_y=True, as_frame=True)
24
24
  model = LinearRegression().fit(X, y.astype(float))
25
25
  path = tmp_path / "reg.pkl"
@@ -84,7 +84,7 @@ def test_mean_averages_numeric_output(reg_tools):
84
84
  ensemble = ModelEnsemble(tools=reg_tools, name="mean_e", description="", strategy="mean")
85
85
  result = ensemble.invoke(SAMPLE)
86
86
  assert "value" in result
87
- # Two identical LinearRegression models mean == either model's output
87
+ # Two identical LinearRegression models mean the average equals either model's output.
88
88
  single = reg_tools[0].invoke(SAMPLE)
89
89
  assert abs(result["value"] - single["value"]) < 1e-6
90
90
 
@@ -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]))
@@ -86,6 +86,27 @@ def test_invoke_bool_string_coerced_through_invoke():
86
86
  assert tool.invoke({"value": 3.0, "active": "on"})["result"] in [0, 1]
87
87
 
88
88
 
89
+ def test_invoke_optional_scalar_strings_coerced_through_invoke():
90
+ class MaybeFlagInput(BaseModel):
91
+ value: float | None = None
92
+ active: bool | None = None
93
+
94
+ X = np.array([[1.0, 0], [2.0, 1], [3.0, 0], [4.0, 1]])
95
+ y = np.array([0, 1, 0, 1])
96
+ clf = LogisticRegression().fit(X, y)
97
+
98
+ tool = ModelTool(
99
+ model=clf,
100
+ name="maybe_flag_test",
101
+ description="test optional scalar coercion",
102
+ input_schema=MaybeFlagInput,
103
+ output_name="result",
104
+ output_description="predicted class",
105
+ )
106
+
107
+ assert tool.invoke({"value": "3.0", "active": "yes"})["result"] in [0, 1]
108
+
109
+
89
110
  SAMPLE = {"sepal_length": 5.1, "sepal_width": 3.5, "petal_length": 1.4, "petal_width": 0.2}
90
111
 
91
112
 
@@ -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 = [
@@ -2984,7 +2984,7 @@ wheels = [
2984
2984
 
2985
2985
  [[package]]
2986
2986
  name = "predikit"
2987
- version = "0.4.5"
2987
+ version = "0.4.6"
2988
2988
  source = { editable = "." }
2989
2989
  dependencies = [
2990
2990
  { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
@@ -1,23 +0,0 @@
1
- artifact_path: file:C:/AI/predikit/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts
2
- flavors:
3
- python_function:
4
- env:
5
- conda: conda.yaml
6
- virtualenv: python_env.yaml
7
- loader_module: mlflow.sklearn
8
- model_path: model.skops
9
- predict_fn: predict
10
- python_version: 3.13.5
11
- sklearn:
12
- code: null
13
- pickled_model: model.skops
14
- serialization_format: skops
15
- sklearn_version: 1.7.2
16
- skops_trusted_types: null
17
- mlflow_version: 3.14.0
18
- model_id: m-27a74bc0ab2f40e09dc3d6c4f026cc1e
19
- model_size_bytes: 9581
20
- model_uuid: m-27a74bc0ab2f40e09dc3d6c4f026cc1e
21
- prompts: null
22
- run_id: cf4fc6366185416489e3a600ec4d813d
23
- utc_time_created: '2026-06-20 21:48:25.168465'
@@ -1,22 +0,0 @@
1
- channels:
2
- - conda-forge
3
- dependencies:
4
- - python=3.13.5
5
- - pip
6
- - pip:
7
- - mlflow==3.14.0
8
- - annotated-types==0.7.0
9
- - joblib==1.5.3
10
- - numpy==2.2.6; python_full_version < "3.11"
11
- - numpy==2.4.6; python_full_version >= "3.11"
12
- - pydantic-core==2.46.4
13
- - pydantic==2.13.4
14
- - scikit-learn==1.7.2
15
- - scipy==1.15.3; python_full_version < "3.11"
16
- - scipy==1.17.1; python_full_version == "3.11.*"
17
- - scipy==1.18.0; python_full_version >= "3.12"
18
- - skops==0.14.0
19
- - threadpoolctl==3.6.0
20
- - typing-extensions==4.15.0
21
- - typing-inspection==0.4.2
22
- name: mlflow-env
@@ -1,7 +0,0 @@
1
- python: 3.13.5
2
- build_dependencies:
3
- - pip
4
- - setuptools
5
- - wheel
6
- dependencies:
7
- - -r requirements.txt
@@ -1,15 +0,0 @@
1
- mlflow==3.14.0
2
- annotated-types==0.7.0
3
- joblib==1.5.3
4
- numpy==2.2.6; python_full_version < "3.11"
5
- numpy==2.4.6; python_full_version >= "3.11"
6
- pydantic-core==2.46.4
7
- pydantic==2.13.4
8
- scikit-learn==1.7.2
9
- scipy==1.15.3; python_full_version < "3.11"
10
- scipy==1.17.1; python_full_version == "3.11.*"
11
- scipy==1.18.0; python_full_version >= "3.12"
12
- skops==0.14.0
13
- threadpoolctl==3.6.0
14
- typing-extensions==4.15.0
15
- typing-inspection==0.4.2
@@ -1,23 +0,0 @@
1
- artifact_path: file:C:/AI/predikit/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts
2
- flavors:
3
- python_function:
4
- env:
5
- conda: conda.yaml
6
- virtualenv: python_env.yaml
7
- loader_module: mlflow.sklearn
8
- model_path: model.skops
9
- predict_fn: predict
10
- python_version: 3.13.5
11
- sklearn:
12
- code: null
13
- pickled_model: model.skops
14
- serialization_format: skops
15
- sklearn_version: 1.7.2
16
- skops_trusted_types: null
17
- mlflow_version: 3.14.0
18
- model_id: m-28173468f0874040a705d2d3820a4f5c
19
- model_size_bytes: 9581
20
- model_uuid: m-28173468f0874040a705d2d3820a4f5c
21
- prompts: null
22
- run_id: c0cfa92fb12c454fb2715f1ef48be4ff
23
- utc_time_created: '2026-06-20 21:47:19.880624'
@@ -1,22 +0,0 @@
1
- channels:
2
- - conda-forge
3
- dependencies:
4
- - python=3.13.5
5
- - pip
6
- - pip:
7
- - mlflow==3.14.0
8
- - annotated-types==0.7.0
9
- - joblib==1.5.3
10
- - numpy==2.2.6; python_full_version < "3.11"
11
- - numpy==2.4.6; python_full_version >= "3.11"
12
- - pydantic-core==2.46.4
13
- - pydantic==2.13.4
14
- - scikit-learn==1.7.2
15
- - scipy==1.15.3; python_full_version < "3.11"
16
- - scipy==1.17.1; python_full_version == "3.11.*"
17
- - scipy==1.18.0; python_full_version >= "3.12"
18
- - skops==0.14.0
19
- - threadpoolctl==3.6.0
20
- - typing-extensions==4.15.0
21
- - typing-inspection==0.4.2
22
- name: mlflow-env
@@ -1,7 +0,0 @@
1
- python: 3.13.5
2
- build_dependencies:
3
- - pip
4
- - setuptools
5
- - wheel
6
- dependencies:
7
- - -r requirements.txt
@@ -1,15 +0,0 @@
1
- mlflow==3.14.0
2
- annotated-types==0.7.0
3
- joblib==1.5.3
4
- numpy==2.2.6; python_full_version < "3.11"
5
- numpy==2.4.6; python_full_version >= "3.11"
6
- pydantic-core==2.46.4
7
- pydantic==2.13.4
8
- scikit-learn==1.7.2
9
- scipy==1.15.3; python_full_version < "3.11"
10
- scipy==1.17.1; python_full_version == "3.11.*"
11
- scipy==1.18.0; python_full_version >= "3.12"
12
- skops==0.14.0
13
- threadpoolctl==3.6.0
14
- typing-extensions==4.15.0
15
- typing-inspection==0.4.2
@@ -1,23 +0,0 @@
1
- artifact_path: file:c:/AI/predikit/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts
2
- flavors:
3
- python_function:
4
- env:
5
- conda: conda.yaml
6
- virtualenv: python_env.yaml
7
- loader_module: mlflow.sklearn
8
- model_path: model.skops
9
- predict_fn: predict
10
- python_version: 3.13.5
11
- sklearn:
12
- code: null
13
- pickled_model: model.skops
14
- serialization_format: skops
15
- sklearn_version: 1.7.2
16
- skops_trusted_types: null
17
- mlflow_version: 3.14.0
18
- model_id: m-3eabd93b6ac447808321a1d58fef2b9b
19
- model_size_bytes: 9581
20
- model_uuid: m-3eabd93b6ac447808321a1d58fef2b9b
21
- prompts: null
22
- run_id: ede0865607194eba8f4bcb00ba747744
23
- utc_time_created: '2026-06-20 21:49:57.444715'
@@ -1,22 +0,0 @@
1
- channels:
2
- - conda-forge
3
- dependencies:
4
- - python=3.13.5
5
- - pip
6
- - pip:
7
- - mlflow==3.14.0
8
- - annotated-types==0.7.0
9
- - joblib==1.5.3
10
- - numpy==2.2.6; python_full_version < "3.11"
11
- - numpy==2.4.6; python_full_version >= "3.11"
12
- - pydantic-core==2.46.4
13
- - pydantic==2.13.4
14
- - scikit-learn==1.7.2
15
- - scipy==1.15.3; python_full_version < "3.11"
16
- - scipy==1.17.1; python_full_version == "3.11.*"
17
- - scipy==1.18.0; python_full_version >= "3.12"
18
- - skops==0.14.0
19
- - threadpoolctl==3.6.0
20
- - typing-extensions==4.15.0
21
- - typing-inspection==0.4.2
22
- name: mlflow-env
@@ -1,7 +0,0 @@
1
- python: 3.13.5
2
- build_dependencies:
3
- - pip
4
- - setuptools
5
- - wheel
6
- dependencies:
7
- - -r requirements.txt
@@ -1,15 +0,0 @@
1
- mlflow==3.14.0
2
- annotated-types==0.7.0
3
- joblib==1.5.3
4
- numpy==2.2.6; python_full_version < "3.11"
5
- numpy==2.4.6; python_full_version >= "3.11"
6
- pydantic-core==2.46.4
7
- pydantic==2.13.4
8
- scikit-learn==1.7.2
9
- scipy==1.15.3; python_full_version < "3.11"
10
- scipy==1.17.1; python_full_version == "3.11.*"
11
- scipy==1.18.0; python_full_version >= "3.12"
12
- skops==0.14.0
13
- threadpoolctl==3.6.0
14
- typing-extensions==4.15.0
15
- typing-inspection==0.4.2
@@ -1,23 +0,0 @@
1
- artifact_path: file:c:/AI/predikit/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts
2
- flavors:
3
- python_function:
4
- env:
5
- conda: conda.yaml
6
- virtualenv: python_env.yaml
7
- loader_module: mlflow.sklearn
8
- model_path: model.skops
9
- predict_fn: predict
10
- python_version: 3.13.5
11
- sklearn:
12
- code: null
13
- pickled_model: model.skops
14
- serialization_format: skops
15
- sklearn_version: 1.7.2
16
- skops_trusted_types: null
17
- mlflow_version: 3.14.0
18
- model_id: m-4816da14ba9c47c2b69d7d959f186d4e
19
- model_size_bytes: 9581
20
- model_uuid: m-4816da14ba9c47c2b69d7d959f186d4e
21
- prompts: null
22
- run_id: e049ecc2fa2d4e1a920d7aed96dde64f
23
- utc_time_created: '2026-06-20 21:50:39.894015'