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.
- {predikit-0.4.4 → predikit-0.4.6}/.gitignore +6 -5
- {predikit-0.4.4 → predikit-0.4.6}/CHANGELOG.md +27 -5
- {predikit-0.4.4 → predikit-0.4.6}/PKG-INFO +2 -2
- {predikit-0.4.4 → predikit-0.4.6}/examples/01_basic_sklearn.py +4 -1
- {predikit-0.4.4 → predikit-0.4.6}/examples/02_xgboost_regression.py +12 -9
- {predikit-0.4.4 → predikit-0.4.6}/examples/03_orlando_real_estate.py +31 -26
- {predikit-0.4.4 → predikit-0.4.6}/examples/04_confidence_routing.py +6 -3
- {predikit-0.4.4 → predikit-0.4.6}/examples/05_multi_model_ensemble.py +58 -31
- {predikit-0.4.4 → predikit-0.4.6}/examples/06_mlflow_loader.py +3 -4
- {predikit-0.4.4 → predikit-0.4.6}/examples/07_snowflake_loader.py +5 -3
- {predikit-0.4.4 → predikit-0.4.6}/pyproject.toml +8 -4
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/__init__.py +1 -1
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/cli.py +2 -2
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/ensemble.py +19 -0
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/registry.py +13 -4
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/tool.py +22 -3
- {predikit-0.4.4 → predikit-0.4.6}/tests/test_cli.py +1 -1
- {predikit-0.4.4 → predikit-0.4.6}/tests/test_confidence.py +11 -6
- {predikit-0.4.4 → predikit-0.4.6}/tests/test_ensemble.py +29 -1
- {predikit-0.4.4 → predikit-0.4.6}/tests/test_loaders_mlflow.py +1 -0
- predikit-0.4.6/tests/test_registry.py +89 -0
- {predikit-0.4.4 → predikit-0.4.6}/tests/test_tool.py +21 -0
- {predikit-0.4.4 → predikit-0.4.6}/tests/test_weighted_ensemble.py +22 -0
- {predikit-0.4.4 → predikit-0.4.6}/uv.lock +1 -1
- predikit-0.4.4/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/requirements.txt +0 -15
- predikit-0.4.4/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/MLmodel +0 -23
- predikit-0.4.4/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/conda.yaml +0 -22
- predikit-0.4.4/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/model.skops +0 -0
- predikit-0.4.4/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/python_env.yaml +0 -7
- predikit-0.4.4/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/requirements.txt +0 -15
- predikit-0.4.4/tests/test_registry.py +0 -45
- {predikit-0.4.4 → predikit-0.4.6}/.claude/settings.local.json +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/.github/workflows/publish.yml +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/.github/workflows/test.yml +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/.pre-commit-config.yaml +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/CLAUDE.md +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/CONTRIBUTING.md +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/LICENSE +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/README.md +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/docs/logo.gif +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/docs/logo.png +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/coerce.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/exceptions.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/exporters/__init__.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/exporters/langchain.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/exporters/openai.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/introspect.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/loaders/__init__.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/loaders/mlflow.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/src/predikit/loaders/snowflake.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/tests/__init__.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/tests/test_coerce.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/tests/test_exporters_openai.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/tests/test_introspect.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/tests/test_loaders_snowflake.py +0 -0
- {predikit-0.4.4 → predikit-0.4.6}/tests/test_logging.py +0 -0
|
@@ -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.
|
|
10
|
-
|
|
11
|
-
###
|
|
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
|
-
Summary: Wrap any trained sklearn/XGBoost model as an LLM-callable tool
|
|
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(
|
|
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,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "predikit"
|
|
7
|
-
version = "0.4.
|
|
8
|
-
description = "Wrap any trained sklearn/XGBoost model as an LLM-callable tool
|
|
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
|
|
@@ -16,7 +16,7 @@ if _CLICK_AVAILABLE:
|
|
|
16
16
|
|
|
17
17
|
@click.group()
|
|
18
18
|
def cli() -> None:
|
|
19
|
-
"""predikit
|
|
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
|
|
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) ->
|
|
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()] + [
|
|
@@ -4,7 +4,8 @@ import asyncio
|
|
|
4
4
|
import time
|
|
5
5
|
import warnings
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
-
from
|
|
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
|
-
|
|
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,
|
|
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
|