predikit 0.4.4__tar.gz → 0.4.6__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 (122) hide show
  1. {predikit-0.4.4 → predikit-0.4.6}/.gitignore +6 -5
  2. {predikit-0.4.4 → predikit-0.4.6}/CHANGELOG.md +27 -5
  3. {predikit-0.4.4 → predikit-0.4.6}/PKG-INFO +2 -2
  4. {predikit-0.4.4 → predikit-0.4.6}/examples/01_basic_sklearn.py +4 -1
  5. {predikit-0.4.4 → predikit-0.4.6}/examples/02_xgboost_regression.py +12 -9
  6. {predikit-0.4.4 → predikit-0.4.6}/examples/03_orlando_real_estate.py +31 -26
  7. {predikit-0.4.4 → predikit-0.4.6}/examples/04_confidence_routing.py +6 -3
  8. {predikit-0.4.4 → predikit-0.4.6}/examples/05_multi_model_ensemble.py +58 -31
  9. {predikit-0.4.4 → predikit-0.4.6}/examples/06_mlflow_loader.py +3 -4
  10. {predikit-0.4.4 → predikit-0.4.6}/examples/07_snowflake_loader.py +5 -3
  11. {predikit-0.4.4 → predikit-0.4.6}/pyproject.toml +8 -4
  12. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/__init__.py +1 -1
  13. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/cli.py +2 -2
  14. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/ensemble.py +19 -0
  15. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/registry.py +13 -4
  16. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/tool.py +22 -3
  17. {predikit-0.4.4 → predikit-0.4.6}/tests/test_cli.py +1 -1
  18. {predikit-0.4.4 → predikit-0.4.6}/tests/test_confidence.py +11 -6
  19. {predikit-0.4.4 → predikit-0.4.6}/tests/test_ensemble.py +29 -1
  20. {predikit-0.4.4 → predikit-0.4.6}/tests/test_loaders_mlflow.py +1 -0
  21. predikit-0.4.6/tests/test_registry.py +89 -0
  22. {predikit-0.4.4 → predikit-0.4.6}/tests/test_tool.py +21 -0
  23. {predikit-0.4.4 → predikit-0.4.6}/tests/test_weighted_ensemble.py +22 -0
  24. {predikit-0.4.4 → predikit-0.4.6}/uv.lock +1 -1
  25. predikit-0.4.4/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/MLmodel +0 -23
  26. predikit-0.4.4/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/conda.yaml +0 -22
  27. predikit-0.4.4/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/model.skops +0 -0
  28. predikit-0.4.4/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/python_env.yaml +0 -7
  29. predikit-0.4.4/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/requirements.txt +0 -15
  30. predikit-0.4.4/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/MLmodel +0 -23
  31. predikit-0.4.4/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/conda.yaml +0 -22
  32. predikit-0.4.4/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/model.skops +0 -0
  33. predikit-0.4.4/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/python_env.yaml +0 -7
  34. predikit-0.4.4/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/requirements.txt +0 -15
  35. predikit-0.4.4/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/MLmodel +0 -23
  36. predikit-0.4.4/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/conda.yaml +0 -22
  37. predikit-0.4.4/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/model.skops +0 -0
  38. predikit-0.4.4/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/python_env.yaml +0 -7
  39. predikit-0.4.4/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/requirements.txt +0 -15
  40. predikit-0.4.4/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/MLmodel +0 -23
  41. predikit-0.4.4/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/conda.yaml +0 -22
  42. predikit-0.4.4/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/model.skops +0 -0
  43. predikit-0.4.4/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/python_env.yaml +0 -7
  44. predikit-0.4.4/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/requirements.txt +0 -15
  45. predikit-0.4.4/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/MLmodel +0 -23
  46. predikit-0.4.4/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/conda.yaml +0 -22
  47. predikit-0.4.4/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/model.skops +0 -0
  48. predikit-0.4.4/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/python_env.yaml +0 -7
  49. predikit-0.4.4/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/requirements.txt +0 -15
  50. predikit-0.4.4/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/MLmodel +0 -23
  51. predikit-0.4.4/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/conda.yaml +0 -22
  52. predikit-0.4.4/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/model.skops +0 -0
  53. predikit-0.4.4/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/python_env.yaml +0 -7
  54. predikit-0.4.4/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/requirements.txt +0 -15
  55. predikit-0.4.4/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/MLmodel +0 -23
  56. predikit-0.4.4/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/conda.yaml +0 -22
  57. predikit-0.4.4/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/model.skops +0 -0
  58. predikit-0.4.4/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/python_env.yaml +0 -7
  59. predikit-0.4.4/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/requirements.txt +0 -15
  60. predikit-0.4.4/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/MLmodel +0 -23
  61. predikit-0.4.4/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/conda.yaml +0 -22
  62. predikit-0.4.4/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/model.skops +0 -0
  63. predikit-0.4.4/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/python_env.yaml +0 -7
  64. predikit-0.4.4/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/requirements.txt +0 -15
  65. predikit-0.4.4/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/MLmodel +0 -23
  66. predikit-0.4.4/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/conda.yaml +0 -22
  67. predikit-0.4.4/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/model.skops +0 -0
  68. predikit-0.4.4/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/python_env.yaml +0 -7
  69. predikit-0.4.4/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/requirements.txt +0 -15
  70. predikit-0.4.4/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/MLmodel +0 -23
  71. predikit-0.4.4/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/conda.yaml +0 -22
  72. predikit-0.4.4/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/model.skops +0 -0
  73. predikit-0.4.4/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/python_env.yaml +0 -7
  74. predikit-0.4.4/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/requirements.txt +0 -15
  75. predikit-0.4.4/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/MLmodel +0 -23
  76. predikit-0.4.4/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/conda.yaml +0 -22
  77. predikit-0.4.4/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/model.skops +0 -0
  78. predikit-0.4.4/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/python_env.yaml +0 -7
  79. predikit-0.4.4/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/requirements.txt +0 -15
  80. predikit-0.4.4/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/MLmodel +0 -23
  81. predikit-0.4.4/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/conda.yaml +0 -22
  82. predikit-0.4.4/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/model.skops +0 -0
  83. predikit-0.4.4/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/python_env.yaml +0 -7
  84. predikit-0.4.4/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/requirements.txt +0 -15
  85. predikit-0.4.4/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/MLmodel +0 -23
  86. predikit-0.4.4/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/conda.yaml +0 -22
  87. predikit-0.4.4/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/model.skops +0 -0
  88. predikit-0.4.4/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/python_env.yaml +0 -7
  89. predikit-0.4.4/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/requirements.txt +0 -15
  90. predikit-0.4.4/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/MLmodel +0 -23
  91. predikit-0.4.4/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/conda.yaml +0 -22
  92. predikit-0.4.4/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/model.skops +0 -0
  93. predikit-0.4.4/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/python_env.yaml +0 -7
  94. predikit-0.4.4/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/requirements.txt +0 -15
  95. predikit-0.4.4/tests/test_registry.py +0 -45
  96. {predikit-0.4.4 → predikit-0.4.6}/.claude/settings.local.json +0 -0
  97. {predikit-0.4.4 → predikit-0.4.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  98. {predikit-0.4.4 → predikit-0.4.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  99. {predikit-0.4.4 → predikit-0.4.6}/.github/workflows/publish.yml +0 -0
  100. {predikit-0.4.4 → predikit-0.4.6}/.github/workflows/test.yml +0 -0
  101. {predikit-0.4.4 → predikit-0.4.6}/.pre-commit-config.yaml +0 -0
  102. {predikit-0.4.4 → predikit-0.4.6}/CLAUDE.md +0 -0
  103. {predikit-0.4.4 → predikit-0.4.6}/CONTRIBUTING.md +0 -0
  104. {predikit-0.4.4 → predikit-0.4.6}/LICENSE +0 -0
  105. {predikit-0.4.4 → predikit-0.4.6}/README.md +0 -0
  106. {predikit-0.4.4 → predikit-0.4.6}/docs/logo.gif +0 -0
  107. {predikit-0.4.4 → predikit-0.4.6}/docs/logo.png +0 -0
  108. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/coerce.py +0 -0
  109. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/exceptions.py +0 -0
  110. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/exporters/__init__.py +0 -0
  111. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/exporters/langchain.py +0 -0
  112. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/exporters/openai.py +0 -0
  113. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/introspect.py +0 -0
  114. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/loaders/__init__.py +0 -0
  115. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/loaders/mlflow.py +0 -0
  116. {predikit-0.4.4 → predikit-0.4.6}/src/predikit/loaders/snowflake.py +0 -0
  117. {predikit-0.4.4 → predikit-0.4.6}/tests/__init__.py +0 -0
  118. {predikit-0.4.4 → predikit-0.4.6}/tests/test_coerce.py +0 -0
  119. {predikit-0.4.4 → predikit-0.4.6}/tests/test_exporters_openai.py +0 -0
  120. {predikit-0.4.4 → predikit-0.4.6}/tests/test_introspect.py +0 -0
  121. {predikit-0.4.4 → predikit-0.4.6}/tests/test_loaders_snowflake.py +0 -0
  122. {predikit-0.4.4 → predikit-0.4.6}/tests/test_logging.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
@@ -4,11 +4,33 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
8
-
9
- ## [0.4.4] - 2026-06-24
10
-
11
- ### Changed
7
+ ## [Unreleased]
8
+
9
+ ## [0.4.6] - 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
+
19
+ ## [0.4.5] - 2026-07-01
20
+
21
+ ### Fixed
22
+ - Added validation for `confidence_threshold` so values outside `0.0` to `1.0` fail at construction time
23
+ - Added `ModelEnsemble` validation for duplicate `collect` outputs, mismatched aggregate output names, negative weights, and zero-total weighted strategies
24
+ - Added `ToolRegistry` duplicate-name validation across tools and ensembles
25
+ - Updated `ToolRegistry.get()` to retrieve both tools and ensembles with clearer missing-name errors
26
+ - Cleaned up example lint issues and corrected stale `modelbridge[xgboost]` install text
27
+
28
+ ### Changed
29
+ - Marked MLflow loader tests as opt-in integration tests so the default pytest suite stays fast and deterministic
30
+
31
+ ## [0.4.4] - 2026-06-24
32
+
33
+ ### Changed
12
34
  - README overhauled: added "Why predikit?" comparison table, "Works with" ecosystem section, GitHub star/fork badges, and a shipped-vs-planned roadmap
13
35
  - Quick start section replaces "30-second example" heading; `---` dividers added between major sections for better scanability
14
36
  - PyPI package description updated to a more descriptive one-liner
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: predikit
3
- Version: 0.4.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.
3
+ Version: 0.4.6
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
@@ -1,4 +1,5 @@
1
1
  """Example 01 — Basic sklearn classifier wrapped as an OpenAI tool."""
2
+
2
3
  import json
3
4
 
4
5
  from pydantic import BaseModel, Field
@@ -36,7 +37,9 @@ print(json.dumps(tool.to_openai(), indent=2))
36
37
 
37
38
  # 5. Direct invocation
38
39
  print("\n=== Direct Invocation ===")
39
- result = tool.invoke({"sepal_length": 5.1, "sepal_width": 3.5, "petal_length": 1.4, "petal_width": 0.2})
40
+ result = tool.invoke(
41
+ {"sepal_length": 5.1, "sepal_width": 3.5, "petal_length": 1.4, "petal_width": 0.2}
42
+ )
40
43
  print(f"Result: {result}")
41
44
 
42
45
  # 6. Plain callable
@@ -1,7 +1,8 @@
1
1
  """
2
2
  Example 02 — XGBoost regression model wrapped as a tool.
3
- Requires: pip install modelbridge[xgboost]
3
+ Requires: pip install predikit[xgboost]
4
4
  """
5
+
5
6
  import json
6
7
 
7
8
  from pydantic import BaseModel, Field
@@ -10,8 +11,8 @@ from sklearn.model_selection import train_test_split
10
11
 
11
12
  try:
12
13
  from xgboost import XGBRegressor
13
- except ImportError:
14
- raise SystemExit("XGBoost not installed. Run: pip install modelbridge[xgboost]")
14
+ except ImportError as err:
15
+ raise SystemExit("XGBoost not installed. Run: pip install predikit[xgboost]") from err
15
16
 
16
17
  from predikit import ModelTool
17
18
 
@@ -44,11 +45,13 @@ print("=== OpenAI Tool Schema ===")
44
45
  print(json.dumps(tool.to_openai(), indent=2))
45
46
 
46
47
  print("\n=== Invoke ===")
47
- result = tool.invoke({
48
- "feature_1": float(X_test[0, 0]),
49
- "feature_2": float(X_test[0, 1]),
50
- "feature_3": float(X_test[0, 2]),
51
- "feature_4": float(X_test[0, 3]),
52
- })
48
+ result = tool.invoke(
49
+ {
50
+ "feature_1": float(X_test[0, 0]),
51
+ "feature_2": float(X_test[0, 1]),
52
+ "feature_3": float(X_test[0, 2]),
53
+ "feature_4": float(X_test[0, 3]),
54
+ }
55
+ )
53
56
  print(f"Prediction: {result}")
54
57
  print(f"Actual: {y_test[0]:.2f}")
@@ -7,6 +7,7 @@ schema generation → registry export → direct invocation → callable.
7
7
 
8
8
  Requires: pip install xgboost
9
9
  """
10
+
10
11
  import json
11
12
 
12
13
  import numpy as np
@@ -16,8 +17,8 @@ from sklearn.model_selection import train_test_split
16
17
 
17
18
  try:
18
19
  from xgboost import XGBRegressor
19
- except ImportError:
20
- raise SystemExit("XGBoost not installed. Run: pip install xgboost")
20
+ except ImportError as err:
21
+ raise SystemExit("XGBoost not installed. Run: pip install xgboost") from err
21
22
 
22
23
  from predikit import ModelTool, ToolRegistry
23
24
 
@@ -27,31 +28,33 @@ from predikit import ModelTool, ToolRegistry
27
28
  rng = np.random.default_rng(42)
28
29
  n = 1_000
29
30
 
30
- sqft = rng.integers(800, 4_000, n).astype(float)
31
- bedrooms = rng.integers(1, 6, n).astype(float)
32
- bathrooms = rng.choice([1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], n)
33
- year_built = rng.integers(1960, 2024, n).astype(float)
34
- has_pool = rng.integers(0, 2, n).astype(float)
31
+ sqft = rng.integers(800, 4_000, n).astype(float)
32
+ bedrooms = rng.integers(1, 6, n).astype(float)
33
+ bathrooms = rng.choice([1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], n)
34
+ year_built = rng.integers(1960, 2024, n).astype(float)
35
+ has_pool = rng.integers(0, 2, n).astype(float)
35
36
  zip_code_group = rng.integers(0, 5, n).astype(float) # 0=rural … 4=prime
36
37
 
37
38
  price = (
38
39
  80 * sqft
39
40
  + 15_000 * bedrooms
40
41
  + 10_000 * bathrooms
41
- + 500 * (year_built - 1960)
42
+ + 500 * (year_built - 1960)
42
43
  + 20_000 * has_pool
43
44
  + 30_000 * zip_code_group
44
45
  + rng.normal(0, 15_000, n)
45
46
  ).clip(50_000, 1_500_000)
46
47
 
47
- df = pd.DataFrame({
48
- "sqft": sqft,
49
- "bedrooms": bedrooms,
50
- "bathrooms": bathrooms,
51
- "year_built": year_built,
52
- "has_pool": has_pool,
53
- "zip_code_group": zip_code_group,
54
- })
48
+ df = pd.DataFrame(
49
+ {
50
+ "sqft": sqft,
51
+ "bedrooms": bedrooms,
52
+ "bathrooms": bathrooms,
53
+ "year_built": year_built,
54
+ "has_pool": has_pool,
55
+ "zip_code_group": zip_code_group,
56
+ }
57
+ )
55
58
 
56
59
  X_train, X_test, y_train, y_test = train_test_split(df, price, test_size=0.2, random_state=42)
57
60
 
@@ -61,17 +64,19 @@ X_train, X_test, y_train, y_test = train_test_split(df, price, test_size=0.2, ra
61
64
  model = XGBRegressor(n_estimators=200, learning_rate=0.05, random_state=42)
62
65
  model.fit(X_train, y_train)
63
66
 
67
+
64
68
  # ---------------------------------------------------------------------------
65
69
  # 3. Input schema — field names must match DataFrame columns exactly
66
70
  # ---------------------------------------------------------------------------
67
71
  class OrlandoHouseInput(BaseModel):
68
- sqft: float = Field(description="Total square footage of the home")
69
- bedrooms: float = Field(description="Number of bedrooms")
70
- bathrooms: float = Field(description="Number of bathrooms (0.5 increments)")
71
- year_built: float = Field(description="Year the home was built")
72
- has_pool: float = Field(description="1 if the home has a pool, else 0")
72
+ sqft: float = Field(description="Total square footage of the home")
73
+ bedrooms: float = Field(description="Number of bedrooms")
74
+ bathrooms: float = Field(description="Number of bathrooms (0.5 increments)")
75
+ year_built: float = Field(description="Year the home was built")
76
+ has_pool: float = Field(description="1 if the home has a pool, else 0")
73
77
  zip_code_group: float = Field(description="Area cluster 0–4 (0=rural, 4=prime location)")
74
78
 
79
+
75
80
  # ---------------------------------------------------------------------------
76
81
  # 4. Wrap as ModelTool and register
77
82
  # ---------------------------------------------------------------------------
@@ -99,11 +104,11 @@ print(json.dumps(registry.to_openai(), indent=2))
99
104
  # 6. Run a prediction
100
105
  # ---------------------------------------------------------------------------
101
106
  sample = {
102
- "sqft": 2_200.0,
103
- "bedrooms": 3.0,
104
- "bathrooms": 2.0,
105
- "year_built": 2005.0,
106
- "has_pool": 1.0,
107
+ "sqft": 2_200.0,
108
+ "bedrooms": 3.0,
109
+ "bathrooms": 2.0,
110
+ "year_built": 2005.0,
111
+ "has_pool": 1.0,
107
112
  "zip_code_group": 3.0,
108
113
  }
109
114
 
@@ -9,6 +9,7 @@ Demonstrates the three on_low_confidence modes:
9
9
  threshold=0.999 guarantees the low-confidence path fires for illustration.
10
10
  In production, set this between 0.75 and 0.95.
11
11
  """
12
+
12
13
  from pydantic import BaseModel, Field
13
14
  from sklearn.datasets import load_iris
14
15
  from sklearn.linear_model import LogisticRegression
@@ -26,9 +27,9 @@ fallback_clf = GaussianNB().fit(X, y)
26
27
 
27
28
  class IrisInput(BaseModel):
28
29
  sepal_length: float = Field(description="Sepal length in cm")
29
- sepal_width: float = Field(description="Sepal width in cm")
30
+ sepal_width: float = Field(description="Sepal width in cm")
30
31
  petal_length: float = Field(description="Petal length in cm")
31
- petal_width: float = Field(description="Petal width in cm")
32
+ petal_width: float = Field(description="Petal width in cm")
32
33
 
33
34
 
34
35
  fallback_tool = ModelTool(
@@ -61,7 +62,9 @@ print("=== Mode: warn ===")
61
62
  result = warn_tool.invoke(sample)
62
63
  print(result)
63
64
  if result.get("_low_confidence"):
64
- print(f" → confidence {result['_confidence']:.3f} below threshold; agent should seek clarification")
65
+ print(
66
+ f" → confidence {result['_confidence']:.3f} below threshold; agent should seek clarification"
67
+ )
65
68
 
66
69
  # ---------------------------------------------------------------------------
67
70
  # Mode 2: raise — lets the calling agent handle uncertainty explicitly
@@ -8,6 +8,7 @@ Demonstrates ModelEnsemble with all three strategies:
8
8
 
9
9
  Requires: pip install predikit[xgboost]
10
10
  """
11
+
11
12
  import json
12
13
 
13
14
  import numpy as np
@@ -19,8 +20,8 @@ from sklearn.model_selection import train_test_split
19
20
 
20
21
  try:
21
22
  from xgboost import XGBRegressor
22
- except ImportError:
23
- raise SystemExit("XGBoost not installed. Run: pip install predikit[xgboost]")
23
+ except ImportError as err:
24
+ raise SystemExit("XGBoost not installed. Run: pip install predikit[xgboost]") from err
24
25
 
25
26
  from predikit import ModelEnsemble, ModelTool, ToolRegistry
26
27
 
@@ -29,37 +30,51 @@ from predikit import ModelEnsemble, ModelTool, ToolRegistry
29
30
  # ---------------------------------------------------------------------------
30
31
  rng = np.random.default_rng(42)
31
32
  n = 1_000
32
- sqft = rng.integers(800, 4_000, n).astype(float)
33
- bedrooms = rng.integers(1, 6, n).astype(float)
34
- bathrooms = rng.choice([1.0, 1.5, 2.0, 2.5, 3.0], n)
35
- year_built = rng.integers(1960, 2024, n).astype(float)
36
- has_pool = rng.integers(0, 2, n).astype(float)
33
+ sqft = rng.integers(800, 4_000, n).astype(float)
34
+ bedrooms = rng.integers(1, 6, n).astype(float)
35
+ bathrooms = rng.choice([1.0, 1.5, 2.0, 2.5, 3.0], n)
36
+ year_built = rng.integers(1960, 2024, n).astype(float)
37
+ has_pool = rng.integers(0, 2, n).astype(float)
37
38
  zip_code_group = rng.integers(0, 5, n).astype(float)
38
39
  price = (
39
- 80 * sqft + 15_000 * bedrooms + 10_000 * bathrooms
40
- + 500 * (year_built - 1960) + 20_000 * has_pool
41
- + 30_000 * zip_code_group + rng.normal(0, 15_000, n)
40
+ 80 * sqft
41
+ + 15_000 * bedrooms
42
+ + 10_000 * bathrooms
43
+ + 500 * (year_built - 1960)
44
+ + 20_000 * has_pool
45
+ + 30_000 * zip_code_group
46
+ + rng.normal(0, 15_000, n)
42
47
  ).clip(50_000, 1_500_000)
43
48
 
44
- df = pd.DataFrame({
45
- "sqft": sqft, "bedrooms": bedrooms, "bathrooms": bathrooms,
46
- "year_built": year_built, "has_pool": has_pool, "zip_code_group": zip_code_group,
47
- })
49
+ df = pd.DataFrame(
50
+ {
51
+ "sqft": sqft,
52
+ "bedrooms": bedrooms,
53
+ "bathrooms": bathrooms,
54
+ "year_built": year_built,
55
+ "has_pool": has_pool,
56
+ "zip_code_group": zip_code_group,
57
+ }
58
+ )
48
59
  X_train, X_test, y_train, _ = train_test_split(df, price, test_size=0.2, random_state=42)
49
60
 
50
61
 
51
62
  class HouseInput(BaseModel):
52
- sqft: float = Field(description="Square footage")
53
- bedrooms: float = Field(description="Number of bedrooms")
54
- bathrooms: float = Field(description="Number of bathrooms")
55
- year_built: float = Field(description="Year built")
56
- has_pool: float = Field(description="1 if has pool, else 0")
63
+ sqft: float = Field(description="Square footage")
64
+ bedrooms: float = Field(description="Number of bedrooms")
65
+ bathrooms: float = Field(description="Number of bathrooms")
66
+ year_built: float = Field(description="Year built")
67
+ has_pool: float = Field(description="1 if has pool, else 0")
57
68
  zip_code_group: float = Field(description="Area cluster 0–4")
58
69
 
59
70
 
60
71
  sample = {
61
- "sqft": 2200.0, "bedrooms": 3.0, "bathrooms": 2.0,
62
- "year_built": 2005.0, "has_pool": 1.0, "zip_code_group": 3.0,
72
+ "sqft": 2200.0,
73
+ "bedrooms": 3.0,
74
+ "bathrooms": 2.0,
75
+ "year_built": 2005.0,
76
+ "has_pool": 1.0,
77
+ "zip_code_group": 3.0,
63
78
  }
64
79
 
65
80
  # ---------------------------------------------------------------------------
@@ -67,13 +82,19 @@ sample = {
67
82
  # ---------------------------------------------------------------------------
68
83
  xgb_tool = ModelTool(
69
84
  model=XGBRegressor(n_estimators=100, random_state=42).fit(X_train, y_train),
70
- name="xgb_price", description="XGBoost price estimate",
71
- input_schema=HouseInput, output_name="xgb_price_usd", output_description="XGBoost price",
85
+ name="xgb_price",
86
+ description="XGBoost price estimate",
87
+ input_schema=HouseInput,
88
+ output_name="xgb_price_usd",
89
+ output_description="XGBoost price",
72
90
  )
73
91
  lr_tool = ModelTool(
74
92
  model=LinearRegression().fit(X_train, y_train),
75
- name="lr_price", description="Linear regression price estimate",
76
- input_schema=HouseInput, output_name="lr_price_usd", output_description="LR price",
93
+ name="lr_price",
94
+ description="Linear regression price estimate",
95
+ input_schema=HouseInput,
96
+ output_name="lr_price_usd",
97
+ output_description="LR price",
77
98
  )
78
99
 
79
100
  collect_ensemble = ModelEnsemble(
@@ -95,8 +116,11 @@ print(f" Linear: ${result['lr_price_usd']:,.0f}")
95
116
  mean_tools = [
96
117
  ModelTool(
97
118
  model=XGBRegressor(n_estimators=100, random_state=seed).fit(X_train, y_train),
98
- name=f"xgb_{seed}", description=f"XGBoost seed={seed}",
99
- input_schema=HouseInput, output_name="price_usd", output_description="price",
119
+ name=f"xgb_{seed}",
120
+ description=f"XGBoost seed={seed}",
121
+ input_schema=HouseInput,
122
+ output_name="price_usd",
123
+ output_description="price",
100
124
  )
101
125
  for seed in [0, 1]
102
126
  ]
@@ -120,16 +144,19 @@ X_iris, y_iris = load_iris(return_X_y=True)
120
144
 
121
145
  class IrisInput(BaseModel):
122
146
  sepal_length: float
123
- sepal_width: float
147
+ sepal_width: float
124
148
  petal_length: float
125
- petal_width: float
149
+ petal_width: float
126
150
 
127
151
 
128
152
  clf_tools = [
129
153
  ModelTool(
130
154
  model=LogisticRegression(max_iter=200, C=c).fit(X_iris, y_iris),
131
- name=f"clf_C{c}", description=f"LR C={c}",
132
- input_schema=IrisInput, output_name="species", output_description="species",
155
+ name=f"clf_C{c}",
156
+ description=f"LR C={c}",
157
+ input_schema=IrisInput,
158
+ output_name="species",
159
+ output_description="species",
133
160
  )
134
161
  for c in [0.1, 1.0, 10.0]
135
162
  ]
@@ -11,17 +11,16 @@ Run:
11
11
  pip install predikit[mlflow]
12
12
  python examples/06_mlflow_loader.py
13
13
  """
14
+
14
15
  import json
15
16
  import tempfile
16
17
 
17
18
  import mlflow
18
19
  import mlflow.sklearn
19
- import numpy as np
20
20
  from pydantic import BaseModel, Field
21
21
  from sklearn.datasets import load_iris
22
22
  from sklearn.linear_model import LogisticRegression
23
23
 
24
- from predikit import LowConfidenceError
25
24
  from predikit.loaders import from_mlflow
26
25
 
27
26
  # ---------------------------------------------------------------------------
@@ -48,9 +47,9 @@ print(f"Logged model → {model_uri}\n")
48
47
  # ---------------------------------------------------------------------------
49
48
  class IrisInput(BaseModel):
50
49
  sepal_length: float = Field(description="Sepal length in cm")
51
- sepal_width: float = Field(description="Sepal width in cm")
50
+ sepal_width: float = Field(description="Sepal width in cm")
52
51
  petal_length: float = Field(description="Petal length in cm")
53
- petal_width: float = Field(description="Petal width in cm")
52
+ petal_width: float = Field(description="Petal width in cm")
54
53
 
55
54
 
56
55
  # ---------------------------------------------------------------------------
@@ -15,13 +15,14 @@ For a real Snowflake connection:
15
15
  # from snowflake.snowpark import Session
16
16
  # session = Session.builder.configs({...}).create()
17
17
  """
18
+
18
19
  import json
19
20
  from unittest.mock import MagicMock
20
21
 
21
22
  import numpy as np
22
23
  from pydantic import BaseModel, Field
23
24
 
24
- from predikit.loaders.snowflake import _SnowflakeShim, from_snowflake
25
+ from predikit.loaders.snowflake import from_snowflake
25
26
 
26
27
  # ---------------------------------------------------------------------------
27
28
  # Mock a Snowflake session and model (stand-in for real Snowpark objects)
@@ -36,6 +37,7 @@ mock_sf_model.predict.return_value = np.array([0.73])
36
37
  def _patch_registry(mock_sf_model):
37
38
  """Context manager that replaces Registry with a stub returning mock_sf_model."""
38
39
  from unittest.mock import patch
40
+
39
41
  registry_mock = MagicMock()
40
42
  registry_mock.get_model.return_value.version.return_value = mock_sf_model
41
43
  return patch("predikit.loaders.snowflake.Registry", return_value=registry_mock)
@@ -45,9 +47,9 @@ def _patch_registry(mock_sf_model):
45
47
  # Define the input schema matching the Snowflake model's feature columns
46
48
  # ---------------------------------------------------------------------------
47
49
  class MemberInput(BaseModel):
48
- tenure_months: float = Field(description="Months as a Vacation Ownership member")
50
+ tenure_months: float = Field(description="Months as a Vacation Ownership member")
49
51
  trips_last_year: float = Field(description="Number of trips taken in the past 12 months")
50
- avg_spend: float = Field(description="Average spend per trip in USD")
52
+ avg_spend: float = Field(description="Average spend per trip in USD")
51
53
 
52
54
 
53
55
  # ---------------------------------------------------------------------------
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "predikit"
7
- version = "0.4.4"
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.4.6"
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"
@@ -70,8 +70,12 @@ Changelog = "https://github.com/Tejas-TA/predikit/releases"
70
70
  [tool.hatch.build.targets.wheel]
71
71
  packages = ["src/predikit"]
72
72
 
73
- [tool.pytest.ini_options]
74
- testpaths = ["tests"]
73
+ [tool.pytest.ini_options]
74
+ testpaths = ["tests"]
75
+ addopts = ["-m", "not integration"]
76
+ markers = [
77
+ "integration: tests that exercise slower external runtimes or registries",
78
+ ]
75
79
 
76
80
  [tool.ruff]
77
81
  line-length = 100
@@ -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.4"
7
+ __version__ = "0.4.6"
@@ -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']}")
@@ -10,6 +10,7 @@ from .exporters.openai import to_openai_schema
10
10
  from .tool import ModelTool
11
11
 
12
12
  _VALID_STRATEGIES = {"collect", "mean", "weighted_mean", "vote", "weighted_vote"}
13
+ _WEIGHTED_STRATEGIES = {"weighted_mean", "weighted_vote"}
13
14
 
14
15
 
15
16
  class ModelEnsemble:
@@ -33,6 +34,24 @@ class ModelEnsemble:
33
34
  raise ValueError(
34
35
  f"weights length ({len(weights)}) must match number of tools ({len(tools)})."
35
36
  )
37
+ if weights is not None:
38
+ if any(w < 0 for w in weights):
39
+ raise ValueError("weights must be non-negative.")
40
+ if strategy in _WEIGHTED_STRATEGIES and sum(weights) <= 0:
41
+ raise ValueError("weights must sum to a positive value.")
42
+ output_names = [tool.output_name for tool in tools]
43
+ if strategy == "collect":
44
+ duplicates = sorted({name for name in output_names if output_names.count(name) > 1})
45
+ if duplicates:
46
+ raise ValueError(
47
+ "collect strategy requires unique output_name values; "
48
+ f"duplicates: {duplicates}."
49
+ )
50
+ elif len(set(output_names)) != 1:
51
+ raise ValueError(
52
+ f"{strategy} strategy requires all tools to share the same output_name; "
53
+ f"got {output_names}."
54
+ )
36
55
  self.tools = tools
37
56
  self.name = name
38
57
  self.description = description
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  from .ensemble import ModelEnsemble
4
4
  from .tool import ModelTool
5
5
 
6
+ RegistryItem = ModelTool | ModelEnsemble
7
+
6
8
 
7
9
  class ToolRegistry:
8
10
  """Bundles multiple ModelTools (and optional ModelEnsembles) for bulk export."""
@@ -12,13 +14,20 @@ class ToolRegistry:
12
14
  tools: list[ModelTool],
13
15
  ensembles: list[ModelEnsemble] | None = None,
14
16
  ) -> None:
17
+ names = [t.name for t in tools] + [e.name for e in (ensembles or [])]
18
+ duplicates = sorted({name for name in names if names.count(name) > 1})
19
+ if duplicates:
20
+ raise ValueError(f"Duplicate tool or ensemble names are not allowed: {duplicates}.")
15
21
  self._tools: dict[str, ModelTool] = {t.name: t for t in tools}
16
22
  self._ensembles: dict[str, ModelEnsemble] = {e.name: e for e in (ensembles or [])}
17
23
 
18
- def get(self, name: str) -> ModelTool:
19
- if name not in self._tools:
20
- raise KeyError(f"No tool named '{name}'. Available: {list(self._tools)}")
21
- return self._tools[name]
24
+ def get(self, name: str) -> RegistryItem:
25
+ if name in self._tools:
26
+ return self._tools[name]
27
+ if name in self._ensembles:
28
+ return self._ensembles[name]
29
+ available = list(self._tools) + list(self._ensembles)
30
+ raise KeyError(f"No tool or ensemble named '{name}'. Available: {available}")
22
31
 
23
32
  def to_openai(self) -> list[dict]:
24
33
  return [t.to_openai() for t in self._tools.values()] + [
@@ -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:
@@ -35,6 +37,10 @@ class ModelTool:
35
37
  fallback_tool: ModelTool | None = None,
36
38
  verbose: bool = False,
37
39
  ) -> None:
40
+ if confidence_threshold is not None and not 0.0 <= confidence_threshold <= 1.0:
41
+ raise ValueError(
42
+ f"confidence_threshold must be between 0.0 and 1.0, got {confidence_threshold!r}"
43
+ )
38
44
  if on_low_confidence not in _VALID_ON_LOW_CONFIDENCE:
39
45
  raise ValueError(
40
46
  f"on_low_confidence must be one of {_VALID_ON_LOW_CONFIDENCE}, got {on_low_confidence!r}"
@@ -139,9 +145,10 @@ class ModelTool:
139
145
  for k, v in input_dict.items():
140
146
  field = fields.get(k)
141
147
  annotation = getattr(field, "annotation", None) if field else None
142
- if annotation in _SCALAR_TYPES:
148
+ scalar_type = _scalar_type(annotation)
149
+ if scalar_type is not None and v is not None:
143
150
  try:
144
- result[k] = coerce_value(v, annotation)
151
+ result[k] = coerce_value(v, scalar_type)
145
152
  except (ValueError, TypeError):
146
153
  result[k] = v
147
154
  else:
@@ -158,3 +165,15 @@ class ModelTool:
158
165
  except ImportError:
159
166
  pass
160
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