diff --git a/docs/docs/api/models/Embedder.md b/docs/docs/api/models/Embedder.md index 48a2459ab5..4bdf4f1a52 100644 --- a/docs/docs/api/models/Embedder.md +++ b/docs/docs/api/models/Embedder.md @@ -1,5 +1,8 @@ # dspy.Embedder +!!! note "Requires numpy" + `dspy.Embedder` requires numpy. Install it with `pip install dspy[numpy]`. + ::: dspy.Embedder handler: python diff --git a/docs/docs/api/optimizers/KNN.md b/docs/docs/api/optimizers/KNN.md index d8c3fcf945..1063fe3ac7 100644 --- a/docs/docs/api/optimizers/KNN.md +++ b/docs/docs/api/optimizers/KNN.md @@ -1,5 +1,8 @@ # dspy.KNN +!!! note "Requires numpy" + `dspy.KNN` requires numpy. Install it with `pip install dspy[numpy]`. + ::: dspy.KNN handler: python diff --git a/docs/docs/api/optimizers/KNNFewShot.md b/docs/docs/api/optimizers/KNNFewShot.md index 758a6f8193..0ef3cbe9fe 100644 --- a/docs/docs/api/optimizers/KNNFewShot.md +++ b/docs/docs/api/optimizers/KNNFewShot.md @@ -1,5 +1,8 @@ # dspy.KNNFewShot +!!! note "Requires numpy" + `dspy.KNNFewShot` requires numpy. Install it with `pip install dspy[numpy]`. + ::: dspy.KNNFewShot handler: python diff --git a/docs/docs/api/optimizers/SIMBA.md b/docs/docs/api/optimizers/SIMBA.md index 4e37f48e1a..d2e9f674ab 100644 --- a/docs/docs/api/optimizers/SIMBA.md +++ b/docs/docs/api/optimizers/SIMBA.md @@ -1,5 +1,8 @@ # dspy.SIMBA +!!! note "Requires numpy" + `dspy.SIMBA` requires numpy. Install it with `pip install dspy[numpy]`. + ::: dspy.SIMBA handler: python diff --git a/docs/docs/api/tools/Embeddings.md b/docs/docs/api/tools/Embeddings.md index 6be5d5574c..91cfa865db 100644 --- a/docs/docs/api/tools/Embeddings.md +++ b/docs/docs/api/tools/Embeddings.md @@ -1,5 +1,8 @@ # dspy.retrievers.Embeddings +!!! note "Requires numpy" + `dspy.retrievers.Embeddings` requires numpy. Install it with `pip install dspy[numpy]`. + ::: dspy.Embeddings handler: python diff --git a/docs/docs/tutorials/rag/index.ipynb b/docs/docs/tutorials/rag/index.ipynb index 47d6223f77..b57a36a811 100644 --- a/docs/docs/tutorials/rag/index.ipynb +++ b/docs/docs/tutorials/rag/index.ipynb @@ -8,7 +8,7 @@ "\n", "Let's walk through a quick example of **basic question answering** with and without **retrieval-augmented generation** (RAG) in DSPy. Specifically, let's build **a system for answering Tech questions**, e.g. about Linux or iPhone apps.\n", "\n", - "Install the latest DSPy via `pip install -U dspy` and follow along. If you're looking instead for a conceptual overview of DSPy, this [recent lecture](https://www.youtube.com/live/JEMYuzrKLUw) is a good place to start. You also need to run `pip install datasets`." + "Install the latest DSPy via `pip install -U dspy` and follow along. If you're looking instead for a conceptual overview of DSPy, this [recent lecture](https://www.youtube.com/live/JEMYuzrKLUw) is a good place to start. You also need to run `pip install datasets`. This tutorial uses `dspy.Embedder` and `dspy.retrievers.Embeddings`, which require numpy: `pip install dspy[numpy]`." ] }, { diff --git a/docs/docs/tutorials/tool_use/index.ipynb b/docs/docs/tutorials/tool_use/index.ipynb index dca08d8022..2500252745 100644 --- a/docs/docs/tutorials/tool_use/index.ipynb +++ b/docs/docs/tutorials/tool_use/index.ipynb @@ -8,7 +8,7 @@ "\n", "Let's walk through a quick example of building and prompt-optimizing a DSPy agent for advanced tool use. We'll do this for the challenging task [ToolHop](https://arxiv.org/abs/2501.02506) but with an even stricter evaluation criteria.\n", "\n", - "Install the latest DSPy via `pip install -U dspy` and follow along. You will also need to `pip install func_timeout datasets`." + "Install the latest DSPy via `pip install -U dspy` and follow along. You will also need to `pip install func_timeout datasets`. This tutorial uses `dspy.SIMBA`, which requires numpy: `pip install dspy[numpy]`." ] }, { diff --git a/dspy/clients/embedding.py b/dspy/clients/embedding.py index 4ba65b34a5..0f74407c44 100644 --- a/dspy/clients/embedding.py +++ b/dspy/clients/embedding.py @@ -1,10 +1,13 @@ +from __future__ import annotations + from typing import Any, Callable import litellm -import numpy as np from dspy.clients.cache import request_cache +from dspy.utils.lazy_import import require +np = require("numpy") class Embedder: """DSPy embedding class. diff --git a/dspy/predict/knn.py b/dspy/predict/knn.py index 0e8a3711e9..68f07b3a63 100644 --- a/dspy/predict/knn.py +++ b/dspy/predict/knn.py @@ -1,7 +1,8 @@ -import numpy as np - from dspy.clients import Embedder from dspy.primitives import Example +from dspy.utils.lazy_import import require + +np = require("numpy") class KNN: diff --git a/dspy/retrievers/embeddings.py b/dspy/retrievers/embeddings.py index 82ba02e8bf..e5fcd4eb30 100644 --- a/dspy/retrievers/embeddings.py +++ b/dspy/retrievers/embeddings.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import json import os from typing import Any -import numpy as np - +from dspy.utils.lazy_import import require from dspy.utils.unbatchify import Unbatchify +np = require("numpy") + class Embeddings: """DSPy Embeddings retriever. diff --git a/dspy/teleprompt/copro_optimizer.py b/dspy/teleprompt/copro_optimizer.py index dd3413fedb..b3ce8c1fc9 100644 --- a/dspy/teleprompt/copro_optimizer.py +++ b/dspy/teleprompt/copro_optimizer.py @@ -1,4 +1,5 @@ import logging +import statistics from collections import defaultdict import dspy @@ -142,9 +143,6 @@ def compile(self, student, *, trainset, eval_kwargs): id(p): {"depth": [], "max": [], "average": [], "min": [], "std": []} for p in module.predictors() } - if self.track_stats: - import numpy as np - candidates = {} evaluated_candidates = defaultdict(dict) @@ -254,7 +252,7 @@ def compile(self, student, *, trainset, eval_kwargs): results_latest[id(p_old)]["max"].append(max(latest_scores)) results_latest[id(p_old)]["average"].append(sum(latest_scores) / len(latest_scores)) results_latest[id(p_old)]["min"].append(min(latest_scores)) - results_latest[id(p_old)]["std"].append(np.std(latest_scores)) + results_latest[id(p_old)]["std"].append(statistics.pstdev(latest_scores)) # Now that we've evaluated the candidates, set this predictor to the best performing version # to ensure the next round of scores reflect the best possible version @@ -296,7 +294,7 @@ def compile(self, student, *, trainset, eval_kwargs): results_best[id(p_base)]["max"].append(max(scores)) results_best[id(p_base)]["average"].append(sum(scores) / len(scores)) results_best[id(p_base)]["min"].append(min(scores)) - results_best[id(p_base)]["std"].append(np.std(scores)) + results_best[id(p_base)]["std"].append(statistics.pstdev(scores)) for i in range(shortest_len - 1, -1, -1): # breakpoint() @@ -341,7 +339,7 @@ def compile(self, student, *, trainset, eval_kwargs): results_best[id(predictor)]["max"].append(max(scores)) results_best[id(predictor)]["average"].append(sum(scores) / len(scores)) results_best[id(predictor)]["min"].append(min(scores)) - results_best[id(predictor)]["std"].append(np.std(scores)) + results_best[id(predictor)]["std"].append(statistics.pstdev(scores)) candidates.sort(key=lambda x: x["score"], reverse=True) diff --git a/dspy/teleprompt/gepa/gepa.py b/dspy/teleprompt/gepa/gepa.py index 41fd6de584..0f2eaf9d5f 100644 --- a/dspy/teleprompt/gepa/gepa.py +++ b/dspy/teleprompt/gepa/gepa.py @@ -1,5 +1,6 @@ import inspect import logging +import math import random from dataclasses import dataclass from typing import Any, Literal, Optional, Protocol, Union @@ -443,9 +444,7 @@ def __init__( def auto_budget( self, num_preds, num_candidates, valset_size: int, minibatch_size: int = 35, full_eval_steps: int = 5 ) -> int: - import numpy as np - - num_trials = int(max(2 * (num_preds * 2) * np.log2(num_candidates), 1.5 * num_candidates)) + num_trials = int(max(2 * (num_preds * 2) * math.log2(num_candidates), 1.5 * num_candidates)) if num_trials < 0 or valset_size < 0 or minibatch_size < 0: raise ValueError("num_trials, valset_size, and minibatch_size must be >= 0.") if full_eval_steps < 1: diff --git a/dspy/teleprompt/infer_rules.py b/dspy/teleprompt/infer_rules.py index 2dcb240665..2509152cc3 100644 --- a/dspy/teleprompt/infer_rules.py +++ b/dspy/teleprompt/infer_rules.py @@ -1,8 +1,7 @@ import logging +import math import random -import numpy as np - import dspy from dspy.evaluate.evaluate import Evaluate from dspy.teleprompt import BootstrapFewShot @@ -32,7 +31,7 @@ def compile(self, student, *, teacher=None, trainset, valset=None): all_predictors = [p for p in original_program.predictors() if hasattr(p, "signature")] instructions_list = [p.signature.instructions for p in all_predictors] - best_score = -np.inf + best_score = -math.inf best_program = None for candidate_idx in range(self.num_candidates): diff --git a/dspy/teleprompt/mipro_optimizer_v2.py b/dspy/teleprompt/mipro_optimizer_v2.py index affeb03ff5..0d7c6f6883 100644 --- a/dspy/teleprompt/mipro_optimizer_v2.py +++ b/dspy/teleprompt/mipro_optimizer_v2.py @@ -1,10 +1,9 @@ import logging +import math import random from collections import defaultdict from typing import TYPE_CHECKING, Any, Callable, Literal -import numpy as np - import dspy from dspy.evaluate.evaluate import Evaluate from dspy.propose import GroundedProposer @@ -277,14 +276,13 @@ def compile( def _set_random_seeds(self, seed): self.rng = random.Random(seed) - np.random.seed(seed) def _set_num_trials_from_num_candidates(self, program, zeroshot_opt, num_candidates): num_vars = len(program.predictors()) if not zeroshot_opt: num_vars *= 2 # Account for few-shot examples + instruction variables # Trials = MAX(c*M*log(N), c=2, 3/2*N) - num_trials = int(max(2 * num_vars * np.log2(num_candidates), 1.5 * num_candidates)) + num_trials = int(max(2 * num_vars * math.log2(num_candidates), 1.5 * num_candidates)) return num_trials diff --git a/dspy/teleprompt/simba.py b/dspy/teleprompt/simba.py index a604f5ee17..514f4acb26 100644 --- a/dspy/teleprompt/simba.py +++ b/dspy/teleprompt/simba.py @@ -4,11 +4,12 @@ import random from typing import Any, Callable -import numpy as np - import dspy from dspy.teleprompt.simba_utils import append_a_demo, append_a_rule, prepare_models_for_resampling, wrap_program from dspy.teleprompt.teleprompt import Teleprompter +from dspy.utils.lazy_import import require + +np = require("numpy") logger = logging.getLogger(__name__) diff --git a/dspy/teleprompt/utils.py b/dspy/teleprompt/utils.py index be82f207ec..9fe6a1eaef 100644 --- a/dspy/teleprompt/utils.py +++ b/dspy/teleprompt/utils.py @@ -6,8 +6,6 @@ import shutil import sys -import numpy as np - try: from IPython.core.magics.code import extract_symbols except ImportError: @@ -121,8 +119,8 @@ def get_program_with_highest_avg_score(param_score_dict, fully_evaled_param_comb # Calculate the mean for each combination of categorical parameters, based on past trials results = [] for key, values in param_score_dict.items(): - scores = np.array([v[0] for v in values]) - mean = np.average(scores) + scores = [v[0] for v in values] + mean = sum(scores) / len(scores) program = values[0][1] params = values[0][2] results.append((key, mean, program, params)) @@ -284,9 +282,8 @@ def get_token_usage(model) -> tuple[int, int]: input_tokens.append(_input_tokens) output_tokens.append(_output_tokens) - total_input_tokens = int(np.sum(input_tokens)) - total_output_tokens = int(np.sum(output_tokens)) - + total_input_tokens = sum(input_tokens) + total_output_tokens = sum(output_tokens) return total_input_tokens, total_output_tokens diff --git a/dspy/utils/dummies.py b/dspy/utils/dummies.py index f53bfb86e5..21889b66aa 100644 --- a/dspy/utils/dummies.py +++ b/dspy/utils/dummies.py @@ -1,13 +1,16 @@ +from __future__ import annotations + import random from collections import defaultdict from typing import Any -import numpy as np - from dspy.adapters.chat_adapter import FieldInfoWithName, field_header_pattern from dspy.clients.base_lm import BaseLM from dspy.dsp.utils.utils import dotdict from dspy.signatures.field import OutputField +from dspy.utils.lazy_import import require + +np = require("numpy") class DummyLM(BaseLM): diff --git a/dspy/utils/lazy_import.py b/dspy/utils/lazy_import.py new file mode 100644 index 0000000000..1090011b84 --- /dev/null +++ b/dspy/utils/lazy_import.py @@ -0,0 +1,126 @@ +"""Lazy-import helpers for optional dependencies. + +Optional deps must be importable lazily so that `import dspy` succeeds even +when they are absent. Call sites get a module-level binding that defers the +real import until first attribute access: + + from dspy.utils.lazy_import import require + + np = require("numpy") # zero cost -- no import happens here + np.array([1, 2, 3]) # numpy is loaded on first use + +If the package is not installed, the first attribute access raises +`ImportError` with an install hint. + +The lazy-load machinery is vendored from *lazy_loader* (BSD-3, Scientific +Python team) and uses `importlib.util.LazyLoader` under the hood. +""" + +import functools +import importlib +import importlib.metadata +import importlib.util +import inspect +import sys +import types +from typing import Any + + +def _detect_dspy_dist() -> str: + for dist in ("dspy", "dspy-ai"): + try: + importlib.metadata.version(dist) + return dist + except importlib.metadata.PackageNotFoundError: + continue + return "dspy" + +_INSTALL_HINTS: dict[str, str] = { + "optuna": "optuna", + "mcp": "mcp", + "langchain_core": "langchain", + "weaviate": "weaviate", + "anthropic": "anthropic", + "numpy": "numpy", +} + + +class _MissingModule(types.ModuleType): + """Stand-in returned by `require()` when a package is not installed. + + Raises `ImportError` with an install hint on any attribute access. + Records the original call site so the traceback is actionable. + """ + + def __init__(self, module: str, message: str, frame_data: dict): + super().__init__(module) + self._message = message + self._frame_data = frame_data + + def __getattr__(self, attr: str): + fd = self._frame_data + raise ImportError( + f"{self._message}\n\n" + "This error is lazily reported, having originally occurred in\n" + f" File {fd['filename']}, line {fd['lineno']}, in {fd['function']}\n\n" + f"----> {''.join(fd['code_context'] or '').strip()}" + ) + + +@functools.cache +def is_available(module: str) -> bool: + """Return True if *module* can be imported, without actually importing it.""" + try: + return importlib.util.find_spec(module) is not None + except (ImportError, ValueError): + return False + + +def require(module: str, *, extra: str | None = None, feature: str | None = None) -> Any: + """Return a lazily-loaded module, or a stub that raises on access. + + Safe to call at module level: + + np = require("numpy") + + **Installed** -- returns a `LazyLoader`-wrapped module. The real import + happens on first attribute access; afterwards the object is a plain module. + + **Not installed** -- returns a `_MissingModule` stub. The first attribute + access raises `ImportError` with a `pip install dspy[…]` hint and the + file/line where `require()` was originally called. + + Args: + module: Dotted module path (e.g. `"numpy"`). + extra: Name of the dspy extra that provides this dep. + feature: Label shown in the error (e.g. `"dspy.Embeddings"`). + """ + if module in sys.modules: + return sys.modules[module] + + spec = importlib.util.find_spec(module) + if spec is None or spec.loader is None: + top = module.split(".", 1)[0] + feat = feature or "this feature" + ext = extra or _INSTALL_HINTS.get(top, top) + dist = _detect_dspy_dist() + message = ( + f"{top} is required to use {feat}. " + f"Install with `pip install {dist}[{ext}]` or `pip install {top}`." + ) + parent = inspect.stack()[1] + frame_data = { + "filename": parent.filename, + "lineno": parent.lineno, + "function": parent.function, + "code_context": parent.code_context, + } + del parent + return _MissingModule(module, message, frame_data) + + loader = importlib.util.LazyLoader(spec.loader) + spec.loader = loader + mod = importlib.util.module_from_spec(spec) + sys.modules[module] = mod + loader.exec_module(mod) + return mod diff --git a/pyproject.toml b/pyproject.toml index ec3cd63f14..0874bea76f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,6 @@ dependencies = [ "anyio", "cachetools>=5.5.0", "cloudpickle>=3.1.2", - "numpy>=1.26.0", "gepa[dspy]==0.1.1", ] @@ -44,6 +43,7 @@ weaviate = ["weaviate-client~=4.5.4"] mcp = ["mcp; python_version >= '3.10'"] langchain = ["langchain_core"] optuna = ["optuna>=3.4.0"] +numpy = ["numpy>=1.26.0"] dev = [ "pytest>=6.2.5", "pytest-mock>=3.12.0", @@ -53,6 +53,7 @@ dev = [ "pillow>=10.1.0", "datamodel_code_generator>=0.26.3", "build>=1.0.3", + "numpy>=1.26.0", "litellm>=1.64.0; sys_platform == 'win32' or python_version == '3.14'", "litellm[proxy]>=1.64.0; sys_platform != 'win32' and python_version < '3.14'", # Remove 3.14 condition once uvloop supports ] @@ -62,6 +63,7 @@ test_extras = [ "pandas>=2.1.1", "optuna>=3.4.0", "langchain_core", + "numpy>=1.26.0", ] [tool.setuptools.packages.find] diff --git a/tests/utils/test_lazy_import.py b/tests/utils/test_lazy_import.py new file mode 100644 index 0000000000..49a718058c --- /dev/null +++ b/tests/utils/test_lazy_import.py @@ -0,0 +1,85 @@ +import pytest + +from dspy.utils.lazy_import import _INSTALL_HINTS, _detect_dspy_dist, _MissingModule, is_available, require + + +def test_is_available_true_for_stdlib(): + assert is_available("json") is True + + +def test_is_available_false_for_missing(): + assert is_available("definitely_not_a_real_module_xyz") is False + + +def test_is_available_does_not_import_module(monkeypatch): + import sys + + # Use a stdlib module that dspy never imports, so we can deterministically + # observe whether is_available() triggers an import as a side effect. + target = "mailbox" + monkeypatch.delitem(sys.modules, target, raising=False) + # is_available is @functools.cache'd; clear so we actually exercise find_spec. + is_available.cache_clear() + + assert is_available(target) is True + assert target not in sys.modules + + +def test_require_returns_lazy_module_when_present(): + mod = require("json") + assert mod.dumps({"a": 1}) == '{"a": 1}' + + +def test_require_returns_cached_module(): + import sys + + mod = require("json") + assert mod is sys.modules["json"] + + +def test_require_returns_stub_when_missing(): + stub = require("definitely_not_a_real_module_xyz", feature="dspy.X") + assert isinstance(stub, _MissingModule) + + +def test_require_stub_raises_on_access_with_install_hint(): + dist = _detect_dspy_dist() + stub = require("nonexistent_abc", feature="dspy.Test") + with pytest.raises(ImportError) as exc_info: + stub.something + msg = str(exc_info.value) + assert f"{dist}[nonexistent_abc]" in msg, msg + assert "dspy.Test" in msg + + +def test_require_stub_uses_explicit_extra(): + dist = _detect_dspy_dist() + stub = require("nonexistent_xyz", extra="custom", feature="dspy.X") + with pytest.raises(ImportError) as exc_info: + stub.something + assert f"{dist}[custom]" in str(exc_info.value) + + +def test_require_stub_falls_back_to_module_name(): + dist = _detect_dspy_dist() + stub = require("nonexistent_xyz", feature="dspy.X") + with pytest.raises(ImportError) as exc_info: + stub.something + assert f"{dist}[nonexistent_xyz]" in str(exc_info.value) + + +def test_install_hints_match_pyproject_extras(pytestconfig): + try: + import tomllib + except ModuleNotFoundError: # Python 3.10 + import tomli as tomllib + + pyproject = pytestconfig.rootpath / "pyproject.toml" + data = tomllib.loads(pyproject.read_text()) + extras = set(data["project"]["optional-dependencies"]) + + for module, hint in _INSTALL_HINTS.items(): + assert hint in extras, ( + f"_INSTALL_HINTS[{module!r}] = {hint!r} is not a declared extra in " + f"pyproject.toml (declared: {sorted(extras)})" + ) diff --git a/uv.lock b/uv.lock index 1ca86c2c82..c9d87f4ba4 100644 --- a/uv.lock +++ b/uv.lock @@ -747,8 +747,6 @@ dependencies = [ { name = "json-repair" }, { name = "litellm", version = "1.68.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' and sys_platform != 'win32'" }, { name = "litellm", version = "1.72.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' or sys_platform == 'win32'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "openai", version = "1.75.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' and sys_platform != 'win32'" }, { name = "openai", version = "1.88.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' or sys_platform == 'win32'" }, { name = "orjson" }, @@ -768,6 +766,8 @@ dev = [ { name = "datamodel-code-generator" }, { name = "litellm", version = "1.68.0", source = { registry = "https://pypi.org/simple" }, extra = ["proxy"], marker = "python_full_version < '3.14' and sys_platform != 'win32'" }, { name = "litellm", version = "1.72.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' or sys_platform == 'win32'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pillow" }, { name = "pre-commit" }, { name = "pytest" }, @@ -782,6 +782,10 @@ mcp = [ { name = "mcp", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' and sys_platform != 'win32'" }, { name = "mcp", version = "1.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' or sys_platform == 'win32'" }, ] +numpy = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] optuna = [ { name = "optuna" }, ] @@ -790,6 +794,8 @@ test-extras = [ { name = "langchain-core" }, { name = "mcp", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' and sys_platform != 'win32'" }, { name = "mcp", version = "1.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' or sys_platform == 'win32'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "optuna" }, { name = "pandas" }, ] @@ -816,7 +822,9 @@ requires-dist = [ { name = "litellm", extras = ["proxy"], marker = "python_full_version < '3.14' and sys_platform != 'win32' and extra == 'dev'", specifier = ">=1.64.0" }, { name = "mcp", marker = "python_full_version >= '3.10' and extra == 'mcp'" }, { name = "mcp", marker = "python_full_version >= '3.10' and extra == 'test-extras'" }, - { name = "numpy", specifier = ">=1.26.0" }, + { name = "numpy", marker = "extra == 'dev'", specifier = ">=1.26.0" }, + { name = "numpy", marker = "extra == 'numpy'", specifier = ">=1.26.0" }, + { name = "numpy", marker = "extra == 'test-extras'", specifier = ">=1.26.0" }, { name = "openai", specifier = ">=0.28.1" }, { name = "optuna", marker = "extra == 'optuna'", specifier = ">=3.4.0" }, { name = "optuna", marker = "extra == 'test-extras'", specifier = ">=3.4.0" }, @@ -835,7 +843,7 @@ requires-dist = [ { name = "tqdm", specifier = ">=4.66.1" }, { name = "weaviate-client", marker = "extra == 'weaviate'", specifier = "~=4.5.4" }, ] -provides-extras = ["anthropic", "weaviate", "mcp", "langchain", "optuna", "dev", "test-extras"] +provides-extras = ["anthropic", "weaviate", "mcp", "langchain", "optuna", "numpy", "dev", "test-extras"] [[package]] name = "email-validator"