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.
Files changed (122) hide show
  1. {predikit-0.4.4 → predikit-0.4.5}/CHANGELOG.md +17 -5
  2. {predikit-0.4.4 → predikit-0.4.5}/PKG-INFO +1 -1
  3. {predikit-0.4.4 → predikit-0.4.5}/examples/01_basic_sklearn.py +4 -1
  4. {predikit-0.4.4 → predikit-0.4.5}/examples/02_xgboost_regression.py +12 -9
  5. {predikit-0.4.4 → predikit-0.4.5}/examples/03_orlando_real_estate.py +31 -26
  6. {predikit-0.4.4 → predikit-0.4.5}/examples/04_confidence_routing.py +6 -3
  7. {predikit-0.4.4 → predikit-0.4.5}/examples/05_multi_model_ensemble.py +58 -31
  8. {predikit-0.4.4 → predikit-0.4.5}/examples/06_mlflow_loader.py +3 -4
  9. {predikit-0.4.4 → predikit-0.4.5}/examples/07_snowflake_loader.py +5 -3
  10. {predikit-0.4.4 → predikit-0.4.5}/pyproject.toml +7 -3
  11. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/__init__.py +1 -1
  12. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/ensemble.py +19 -0
  13. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/registry.py +13 -4
  14. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/tool.py +4 -0
  15. {predikit-0.4.4 → predikit-0.4.5}/tests/test_confidence.py +11 -6
  16. {predikit-0.4.4 → predikit-0.4.5}/tests/test_ensemble.py +28 -0
  17. {predikit-0.4.4 → predikit-0.4.5}/tests/test_loaders_mlflow.py +1 -0
  18. predikit-0.4.5/tests/test_registry.py +89 -0
  19. {predikit-0.4.4 → predikit-0.4.5}/tests/test_weighted_ensemble.py +22 -0
  20. {predikit-0.4.4 → predikit-0.4.5}/uv.lock +1 -1
  21. predikit-0.4.4/tests/test_registry.py +0 -45
  22. {predikit-0.4.4 → predikit-0.4.5}/.claude/settings.local.json +0 -0
  23. {predikit-0.4.4 → predikit-0.4.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  24. {predikit-0.4.4 → predikit-0.4.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  25. {predikit-0.4.4 → predikit-0.4.5}/.github/workflows/publish.yml +0 -0
  26. {predikit-0.4.4 → predikit-0.4.5}/.github/workflows/test.yml +0 -0
  27. {predikit-0.4.4 → predikit-0.4.5}/.gitignore +0 -0
  28. {predikit-0.4.4 → predikit-0.4.5}/.pre-commit-config.yaml +0 -0
  29. {predikit-0.4.4 → predikit-0.4.5}/CLAUDE.md +0 -0
  30. {predikit-0.4.4 → predikit-0.4.5}/CONTRIBUTING.md +0 -0
  31. {predikit-0.4.4 → predikit-0.4.5}/LICENSE +0 -0
  32. {predikit-0.4.4 → predikit-0.4.5}/README.md +0 -0
  33. {predikit-0.4.4 → predikit-0.4.5}/docs/logo.gif +0 -0
  34. {predikit-0.4.4 → predikit-0.4.5}/docs/logo.png +0 -0
  35. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/MLmodel +0 -0
  36. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/conda.yaml +0 -0
  37. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/model.skops +0 -0
  38. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/python_env.yaml +0 -0
  39. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-27a74bc0ab2f40e09dc3d6c4f026cc1e/artifacts/requirements.txt +0 -0
  40. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/MLmodel +0 -0
  41. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/conda.yaml +0 -0
  42. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/model.skops +0 -0
  43. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/python_env.yaml +0 -0
  44. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-28173468f0874040a705d2d3820a4f5c/artifacts/requirements.txt +0 -0
  45. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/MLmodel +0 -0
  46. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/conda.yaml +0 -0
  47. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/model.skops +0 -0
  48. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/python_env.yaml +0 -0
  49. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-3eabd93b6ac447808321a1d58fef2b9b/artifacts/requirements.txt +0 -0
  50. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/MLmodel +0 -0
  51. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/conda.yaml +0 -0
  52. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/model.skops +0 -0
  53. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/python_env.yaml +0 -0
  54. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4816da14ba9c47c2b69d7d959f186d4e/artifacts/requirements.txt +0 -0
  55. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/MLmodel +0 -0
  56. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/conda.yaml +0 -0
  57. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/model.skops +0 -0
  58. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/python_env.yaml +0 -0
  59. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-4deda032d5434d74888edc6f4df2e642/artifacts/requirements.txt +0 -0
  60. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/MLmodel +0 -0
  61. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/conda.yaml +0 -0
  62. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/model.skops +0 -0
  63. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/python_env.yaml +0 -0
  64. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-6988b8fc759d4a6d9ba42b3ef9c5bbad/artifacts/requirements.txt +0 -0
  65. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/MLmodel +0 -0
  66. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/conda.yaml +0 -0
  67. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/model.skops +0 -0
  68. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/python_env.yaml +0 -0
  69. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-885ed3e972dd4583aa9b4c518b369316/artifacts/requirements.txt +0 -0
  70. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/MLmodel +0 -0
  71. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/conda.yaml +0 -0
  72. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/model.skops +0 -0
  73. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/python_env.yaml +0 -0
  74. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-936aa30d653c4e79920e98dd23b2e352/artifacts/requirements.txt +0 -0
  75. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/MLmodel +0 -0
  76. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/conda.yaml +0 -0
  77. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/model.skops +0 -0
  78. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/python_env.yaml +0 -0
  79. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-9c8120122eb744128b3c91e724536d7e/artifacts/requirements.txt +0 -0
  80. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/MLmodel +0 -0
  81. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/conda.yaml +0 -0
  82. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/model.skops +0 -0
  83. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/python_env.yaml +0 -0
  84. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-bc53e68577e64ed5b64b173ae23eb99d/artifacts/requirements.txt +0 -0
  85. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/MLmodel +0 -0
  86. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/conda.yaml +0 -0
  87. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/model.skops +0 -0
  88. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/python_env.yaml +0 -0
  89. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-d5e1b20e8a9a4414b74b95d613362ae0/artifacts/requirements.txt +0 -0
  90. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/MLmodel +0 -0
  91. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/conda.yaml +0 -0
  92. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/model.skops +0 -0
  93. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/python_env.yaml +0 -0
  94. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e368d86129cb416cbdc24ce1f4d4062b/artifacts/requirements.txt +0 -0
  95. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/MLmodel +0 -0
  96. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/conda.yaml +0 -0
  97. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/model.skops +0 -0
  98. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/python_env.yaml +0 -0
  99. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-e71aea9d1d2646f0970b78d821200719/artifacts/requirements.txt +0 -0
  100. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/MLmodel +0 -0
  101. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/conda.yaml +0 -0
  102. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/model.skops +0 -0
  103. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/python_env.yaml +0 -0
  104. {predikit-0.4.4 → predikit-0.4.5}/mlruns/1/models/m-f374e93dce4f4be498ccf04e1fb1ac9e/artifacts/requirements.txt +0 -0
  105. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/cli.py +0 -0
  106. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/coerce.py +0 -0
  107. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/exceptions.py +0 -0
  108. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/exporters/__init__.py +0 -0
  109. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/exporters/langchain.py +0 -0
  110. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/exporters/openai.py +0 -0
  111. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/introspect.py +0 -0
  112. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/loaders/__init__.py +0 -0
  113. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/loaders/mlflow.py +0 -0
  114. {predikit-0.4.4 → predikit-0.4.5}/src/predikit/loaders/snowflake.py +0 -0
  115. {predikit-0.4.4 → predikit-0.4.5}/tests/__init__.py +0 -0
  116. {predikit-0.4.4 → predikit-0.4.5}/tests/test_cli.py +0 -0
  117. {predikit-0.4.4 → predikit-0.4.5}/tests/test_coerce.py +0 -0
  118. {predikit-0.4.4 → predikit-0.4.5}/tests/test_exporters_openai.py +0 -0
  119. {predikit-0.4.4 → predikit-0.4.5}/tests/test_introspect.py +0 -0
  120. {predikit-0.4.4 → predikit-0.4.5}/tests/test_loaders_snowflake.py +0 -0
  121. {predikit-0.4.4 → predikit-0.4.5}/tests/test_logging.py +0 -0
  122. {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.4] - 2026-06-24
10
-
11
- ### Changed
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.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({"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,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "predikit"
7
- version = "0.4.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
@@ -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.5"
@@ -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()] + [
@@ -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=2.0 is impossible to exceed (probabilities 1.0), so always fires
46
- result = _make(clf, threshold=2.0, mode="warn").invoke(SAMPLE)
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=2.0, mode="raise").invoke(SAMPLE)
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=2.0, mode="fallback", fallback=fallback).invoke(SAMPLE)
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=2.0, mode="fallback", fallback=None)
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=2.0,
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="")