predikit 0.4.4__tar.gz → 0.4.5__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.
- {predikit-0.4.4 → predikit-0.4.5}/CHANGELOG.md +17 -5
- {predikit-0.4.4 → predikit-0.4.5}/PKG-INFO +1 -1
- {predikit-0.4.4 → predikit-0.4.5}/examples/01_basic_sklearn.py +4 -1
- {predikit-0.4.4 → predikit-0.4.5}/examples/02_xgboost_regression.py +12 -9
- {predikit-0.4.4 → predikit-0.4.5}/examples/03_orlando_real_estate.py +31 -26
- {predikit-0.4.4 → predikit-0.4.5}/examples/04_confidence_routing.py +6 -3
- {predikit-0.4.4 → predikit-0.4.5}/examples/05_multi_model_ensemble.py +58 -31
- {predikit-0.4.4 → predikit-0.4.5}/examples/06_mlflow_loader.py +3 -4
- {predikit-0.4.4 → predikit-0.4.5}/examples/07_snowflake_loader.py +5 -3
- {predikit-0.4.4 → predikit-0.4.5}/pyproject.toml +7 -3
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/__init__.py +1 -1
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/ensemble.py +19 -0
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/registry.py +13 -4
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/tool.py +4 -0
- {predikit-0.4.4 → predikit-0.4.5}/tests/test_confidence.py +11 -6
- {predikit-0.4.4 → predikit-0.4.5}/tests/test_ensemble.py +28 -0
- {predikit-0.4.4 → predikit-0.4.5}/tests/test_loaders_mlflow.py +1 -0
- predikit-0.4.5/tests/test_registry.py +89 -0
- {predikit-0.4.4 → predikit-0.4.5}/tests/test_weighted_ensemble.py +22 -0
- {predikit-0.4.4 → predikit-0.4.5}/uv.lock +1 -1
- predikit-0.4.4/tests/test_registry.py +0 -45
- {predikit-0.4.4 → predikit-0.4.5}/.claude/settings.local.json +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/.github/workflows/publish.yml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/.github/workflows/test.yml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/.gitignore +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/.pre-commit-config.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/CLAUDE.md +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/CONTRIBUTING.md +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/LICENSE +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/README.md +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/docs/logo.gif +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/docs/logo.png +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/MLmodel +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/conda.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/model.skops +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/python_env.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/requirements.txt +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/cli.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/coerce.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/exceptions.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/exporters/__init__.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/exporters/langchain.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/exporters/openai.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/introspect.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/loaders/__init__.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/loaders/mlflow.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/src/predikit/loaders/snowflake.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/tests/__init__.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/tests/test_cli.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/tests/test_coerce.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/tests/test_exporters_openai.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/tests/test_introspect.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/tests/test_loaders_snowflake.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/tests/test_logging.py +0 -0
- {predikit-0.4.4 → predikit-0.4.5}/tests/test_tool.py +0 -0
|
@@ -4,11 +4,23 @@ 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.
|
|
10
|
-
|
|
11
|
-
###
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.4.5] - 2026-07-01
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Added validation for `confidence_threshold` so values outside `0.0` to `1.0` fail at construction time
|
|
13
|
+
- Added `ModelEnsemble` validation for duplicate `collect` outputs, mismatched aggregate output names, negative weights, and zero-total weighted strategies
|
|
14
|
+
- Added `ToolRegistry` duplicate-name validation across tools and ensembles
|
|
15
|
+
- Updated `ToolRegistry.get()` to retrieve both tools and ensembles with clearer missing-name errors
|
|
16
|
+
- Cleaned up example lint issues and corrected stale `modelbridge[xgboost]` install text
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Marked MLflow loader tests as opt-in integration tests so the default pytest suite stays fast and deterministic
|
|
20
|
+
|
|
21
|
+
## [0.4.4] - 2026-06-24
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
12
24
|
- README overhauled: added "Why predikit?" comparison table, "Works with" ecosystem section, GitHub star/fork badges, and a shipped-vs-planned roadmap
|
|
13
25
|
- Quick start section replaces "30-second example" heading; `---` dividers added between major sections for better scanability
|
|
14
26
|
- PyPI package description updated to a more descriptive one-liner
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: predikit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
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
|
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
31
|
-
bedrooms
|
|
32
|
-
bathrooms
|
|
33
|
-
year_built
|
|
34
|
-
has_pool
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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:
|
|
69
|
-
bedrooms:
|
|
70
|
-
bathrooms:
|
|
71
|
-
year_built:
|
|
72
|
-
has_pool:
|
|
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":
|
|
103
|
-
"bedrooms":
|
|
104
|
-
"bathrooms":
|
|
105
|
-
"year_built":
|
|
106
|
-
"has_pool":
|
|
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:
|
|
30
|
+
sepal_width: float = Field(description="Sepal width in cm")
|
|
30
31
|
petal_length: float = Field(description="Petal length in cm")
|
|
31
|
-
petal_width:
|
|
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(
|
|
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
|
|
33
|
-
bedrooms
|
|
34
|
-
bathrooms
|
|
35
|
-
year_built
|
|
36
|
-
has_pool
|
|
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
|
|
40
|
-
+
|
|
41
|
-
+
|
|
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
|
-
|
|
46
|
-
|
|
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:
|
|
53
|
-
bedrooms:
|
|
54
|
-
bathrooms:
|
|
55
|
-
year_built:
|
|
56
|
-
has_pool:
|
|
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,
|
|
62
|
-
"
|
|
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",
|
|
71
|
-
|
|
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",
|
|
76
|
-
|
|
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}",
|
|
99
|
-
|
|
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:
|
|
147
|
+
sepal_width: float
|
|
124
148
|
petal_length: float
|
|
125
|
-
petal_width:
|
|
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}",
|
|
132
|
-
|
|
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:
|
|
50
|
+
sepal_width: float = Field(description="Sepal width in cm")
|
|
52
51
|
petal_length: float = Field(description="Petal length in cm")
|
|
53
|
-
petal_width:
|
|
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
|
|
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:
|
|
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:
|
|
52
|
+
avg_spend: float = Field(description="Average spend per trip in USD")
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
# ---------------------------------------------------------------------------
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "predikit"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.5"
|
|
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"}
|
|
@@ -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
|
|
@@ -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) ->
|
|
19
|
-
if name
|
|
20
|
-
|
|
21
|
-
|
|
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()] + [
|
|
@@ -35,6 +35,10 @@ class ModelTool:
|
|
|
35
35
|
fallback_tool: ModelTool | None = None,
|
|
36
36
|
verbose: bool = False,
|
|
37
37
|
) -> None:
|
|
38
|
+
if confidence_threshold is not None and not 0.0 <= confidence_threshold <= 1.0:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"confidence_threshold must be between 0.0 and 1.0, got {confidence_threshold!r}"
|
|
41
|
+
)
|
|
38
42
|
if on_low_confidence not in _VALID_ON_LOW_CONFIDENCE:
|
|
39
43
|
raise ValueError(
|
|
40
44
|
f"on_low_confidence must be one of {_VALID_ON_LOW_CONFIDENCE}, got {on_low_confidence!r}"
|
|
@@ -42,8 +42,8 @@ def test_above_threshold_returns_clean_result(clf):
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def test_warn_mode_returns_flags(clf):
|
|
45
|
-
# threshold=
|
|
46
|
-
result = _make(clf, threshold=
|
|
45
|
+
# threshold=1.0 is impossible to exceed for real probabilities, so always fires
|
|
46
|
+
result = _make(clf, threshold=1.0, mode="warn").invoke(SAMPLE)
|
|
47
47
|
assert result["_low_confidence"] is True
|
|
48
48
|
assert 0.0 <= result["_confidence"] <= 1.0
|
|
49
49
|
assert "species" in result
|
|
@@ -51,7 +51,7 @@ def test_warn_mode_returns_flags(clf):
|
|
|
51
51
|
|
|
52
52
|
def test_raise_mode_raises_low_confidence_error(clf):
|
|
53
53
|
with pytest.raises(LowConfidenceError, match="below threshold"):
|
|
54
|
-
_make(clf, threshold=
|
|
54
|
+
_make(clf, threshold=1.0, mode="raise").invoke(SAMPLE)
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def test_fallback_mode_invokes_fallback_tool(clf):
|
|
@@ -64,14 +64,14 @@ def test_fallback_mode_invokes_fallback_tool(clf):
|
|
|
64
64
|
output_name="species",
|
|
65
65
|
output_description="species",
|
|
66
66
|
)
|
|
67
|
-
result = _make(clf, threshold=
|
|
67
|
+
result = _make(clf, threshold=1.0, mode="fallback", fallback=fallback).invoke(SAMPLE)
|
|
68
68
|
assert "species" in result
|
|
69
69
|
assert "_low_confidence" not in result
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def test_fallback_without_fallback_tool_raises_at_init(clf):
|
|
73
73
|
with pytest.raises(ValueError, match="fallback_tool"):
|
|
74
|
-
_make(clf, threshold=
|
|
74
|
+
_make(clf, threshold=1.0, mode="fallback", fallback=None)
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
def test_no_threshold_skips_confidence_check(clf):
|
|
@@ -88,7 +88,7 @@ def test_confidence_not_applied_to_regressor():
|
|
|
88
88
|
input_schema=IrisInput,
|
|
89
89
|
output_name="value",
|
|
90
90
|
output_description="value",
|
|
91
|
-
confidence_threshold=
|
|
91
|
+
confidence_threshold=1.0,
|
|
92
92
|
on_low_confidence="raise", # would fire for a classifier
|
|
93
93
|
)
|
|
94
94
|
result = tool.invoke(SAMPLE) # must NOT raise
|
|
@@ -98,3 +98,8 @@ def test_confidence_not_applied_to_regressor():
|
|
|
98
98
|
def test_invalid_on_low_confidence_raises_at_construction(clf):
|
|
99
99
|
with pytest.raises(ValueError, match="on_low_confidence"):
|
|
100
100
|
_make(clf, threshold=0.9, mode="explode")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_invalid_confidence_threshold_raises_at_construction(clf):
|
|
104
|
+
with pytest.raises(ValueError, match="confidence_threshold"):
|
|
105
|
+
_make(clf, threshold=1.1, mode="warn")
|
|
@@ -75,6 +75,11 @@ def test_collect_merges_distinct_output_names():
|
|
|
75
75
|
assert "score" in result
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def test_collect_duplicate_output_names_raises(clf_tools):
|
|
79
|
+
with pytest.raises(ValueError, match="unique output_name"):
|
|
80
|
+
ModelEnsemble(tools=clf_tools, name="e", description="", strategy="collect")
|
|
81
|
+
|
|
82
|
+
|
|
78
83
|
def test_mean_averages_numeric_output(reg_tools):
|
|
79
84
|
ensemble = ModelEnsemble(tools=reg_tools, name="mean_e", description="", strategy="mean")
|
|
80
85
|
result = ensemble.invoke(SAMPLE)
|
|
@@ -96,6 +101,29 @@ def test_invalid_strategy_raises(clf_tools):
|
|
|
96
101
|
ModelEnsemble(tools=clf_tools, name="e", description="", strategy="bad").invoke(SAMPLE)
|
|
97
102
|
|
|
98
103
|
|
|
104
|
+
def test_aggregate_strategy_mismatched_output_names_raises():
|
|
105
|
+
clf = LogisticRegression(max_iter=200).fit(X, y)
|
|
106
|
+
reg = LinearRegression().fit(X, y.astype(float))
|
|
107
|
+
tool_a = ModelTool(
|
|
108
|
+
model=clf,
|
|
109
|
+
name="a",
|
|
110
|
+
description="",
|
|
111
|
+
input_schema=IrisInput,
|
|
112
|
+
output_name="species",
|
|
113
|
+
output_description="",
|
|
114
|
+
)
|
|
115
|
+
tool_b = ModelTool(
|
|
116
|
+
model=reg,
|
|
117
|
+
name="b",
|
|
118
|
+
description="",
|
|
119
|
+
input_schema=IrisInput,
|
|
120
|
+
output_name="score",
|
|
121
|
+
output_description="",
|
|
122
|
+
)
|
|
123
|
+
with pytest.raises(ValueError, match="same output_name"):
|
|
124
|
+
ModelEnsemble(tools=[tool_a, tool_b], name="e", description="", strategy="mean")
|
|
125
|
+
|
|
126
|
+
|
|
99
127
|
def test_empty_tools_raises():
|
|
100
128
|
with pytest.raises(ValueError, match="at least one tool"):
|
|
101
129
|
ModelEnsemble(tools=[], name="e", description="")
|