diff --git a/agentic_security/app.py b/agentic_security/app.py index fe1ed8b..cbb8c1d 100644 --- a/agentic_security/app.py +++ b/agentic_security/app.py @@ -77,6 +77,7 @@ class Scan(BaseModel): llmSpec: str maxBudget: int datasets: list[dict] = [] + optimize: bool = False class ScanResult(BaseModel): @@ -97,6 +98,7 @@ def streaming_response_generator(scan_parameters: Scan): max_budget=scan_parameters.maxBudget, datasets=scan_parameters.datasets, tools_inbox=tools_inbox, + optimize=scan_parameters.optimize, ): yield scan_result + "\n" # Adding a newline for separation diff --git a/agentic_security/probe_actor/fuzzer.py b/agentic_security/probe_actor/fuzzer.py index b8e9fc4..1695b26 100644 --- a/agentic_security/probe_actor/fuzzer.py +++ b/agentic_security/probe_actor/fuzzer.py @@ -1,11 +1,16 @@ import os +import asyncio +from typing import List, Dict, AsyncGenerator import httpx -from loguru import logger -from pydantic import BaseModel - +import numpy as np +import pandas as pd from agentic_security.probe_actor.refusal import refusal_heuristic from agentic_security.probe_data.data import prepare_prompts +from loguru import logger +from pydantic import BaseModel +from skopt import Optimizer +from skopt.space import Real IS_VERCEL = os.getenv("IS_VERCEL", "f") == "t" @@ -19,7 +24,7 @@ class ScanResult(BaseModel): status: bool = False @classmethod - def status_msg(cls, msg: str): + def status_msg(cls, msg: str) -> str: return cls( module=msg, tokens=0, @@ -30,24 +35,29 @@ class ScanResult(BaseModel): ).model_dump_json() -async def prompt_iter(prompts): +async def prompt_iter(prompts: List[str] | AsyncGenerator) -> AsyncGenerator[str, None]: if isinstance(prompts, list): for p in prompts: yield p - return - async for p in prompts: - yield p + else: + async for p in prompts: + yield p async def perform_scan( - request_factory, max_budget: int, datasets: list[dict] = [], tools_inbox=None -): - yield ScanResult.status_msg("Loading datasets...") + request_factory, + max_budget: int, + datasets: List[Dict[str, str]] = [], + tools_inbox=None, + optimize=False, +) -> AsyncGenerator[str, None]: if IS_VERCEL: yield ScanResult.status_msg( - "Vercel deployment detected. Streaming messages are not supported by serverless, plz run it locally." + "Vercel deployment detected. Streaming messages are not supported by serverless, please run it locally." ) return + + yield ScanResult.status_msg("Loading datasets...") prompt_modules = prepare_prompts( dataset_names=[m["dataset_name"] for m in datasets if m["selected"]], budget=max_budget, @@ -57,63 +67,83 @@ async def perform_scan( errors = [] refusals = [] - size = sum(len(m.prompts) for m in prompt_modules if not m.lazy) - step = 0 - for mi, module in enumerate(prompt_modules): + total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy) + processed_prompts = 0 + + failure_rates = [] + + for module in prompt_modules: tokens = 0 module_failures = 0 - size = 0 if module.lazy else len(module.prompts) - logger.info(f"Scanning {module.dataset_name} {size}") - i = 0 + module_size = 0 if module.lazy else len(module.prompts) + logger.info(f"Scanning {module.dataset_name} {module_size}") + optimizer = Optimizer( + [Real(0, 1)], base_estimator="GP", n_initial_points=25, acq_func="EI" + ) + should_stop_early = False async for prompt in prompt_iter(module.prompts): - i += 1 - step += 1 - progress = 100 * (step) / size if size else 0 + processed_prompts += 1 + progress = 100 * processed_prompts / total_prompts if total_prompts else 0 - # Naive token count tokens += len(prompt.split()) try: r = await request_factory.fn(prompt=prompt) - except httpx.RequestError as e: + if r.status_code >= 400: + raise httpx.HTTPStatusError( + f"HTTP {r.status_code}", request=r.request, response=r + ) + + response_text = r.text + tokens += len(response_text.split()) + + if not refusal_heuristic(r.json()): + refusals.append( + (module.dataset_name, prompt, r.status_code, response_text) + ) + module_failures += 1 + except (httpx.RequestError, httpx.HTTPStatusError) as e: logger.error(f"Request error: {e}") - errors.append((module.dataset_name, prompt.replace("\n", ";"), e)) + errors.append((module.dataset_name, prompt, str(e))) module_failures += 1 continue - if r.status_code >= 400: - module_failures += 1 - errors.append( - ( - module.dataset_name, - prompt.replace("\n", ";"), - r.status_code, - r.text, - ) - ) - elif not refusal_heuristic(r.json()): - refusals.append( - ( - module.dataset_name, - prompt.replace("\n", ";"), - r.status_code, - r.text, - ) - ) - module_failures += 1 - # Naive token count for llm response - tokens += len(r.text.split()) - total = size if size else i + + failure_rate = module_failures / max(processed_prompts, 1) + failure_rates.append(failure_rate) + yield ScanResult( module=module.dataset_name, tokens=round(tokens / 1000, 1), cost=round(tokens * 1.5 / 1000_000, 2), progress=round(progress, 2), - failureRate=100 * module_failures / max(total, 1), + failureRate=round(failure_rate * 100, 2), ).model_dump_json() - yield ScanResult.status_msg("Done.") - import pandas as pd + + if not optimize: + continue + # Use the optimizer to decide whether to stop early + if len(failure_rates) >= 5: # Wait for at least 5 data points + next_point = optimizer.ask() + optimizer.tell( + next_point, -failure_rate + ) # We want to minimize failure rate + + # Get the best point found so far + best_failure_rate = -optimizer.get_result().fun + + # If the best failure rate is high, consider stopping + if best_failure_rate > 0.5: # Threshold can be adjusted + yield ScanResult.status_msg( + f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..." + ) + should_stop_early = True + break # Break out of the prompt loop + + if should_stop_early: + continue # Move to the next module + + yield ScanResult.status_msg("Scan completed.") df = pd.DataFrame( errors + refusals, columns=["module", "prompt", "status_code", "content"] ) df.to_csv("failures.csv", index=False) - # TODO: save all results diff --git a/agentic_security/static/index.html b/agentic_security/static/index.html index b7562f0..9f497d4 100644 --- a/agentic_security/static/index.html +++ b/agentic_security/static/index.html @@ -156,26 +156,69 @@ placeholder="Enter LLM API Spec here..."> - -
-

Maximum Budget

-
- 1M Tokens - - 100M Tokens +
+
+
+ + + +

Parameters

+
+ + + +
+
+ + +
+

Maximum Budget

+
+ 1M Tokens + + 100M Tokens +
+ +
+ +
+
+

Optimize Test?

+ +
+

+ When enabled, this option runs a Bayesian optimization loop to + find the most effective test parameters. This can potentially + reduce the cost and the total running time of your vulnerability + scan, but may reduce accuracy. +

+
-
@@ -277,7 +320,7 @@ {{result.module}} - {{getFailureRateScore(result.failureRate)}}( {{(100 - result.failureRate).toFixed(2)}} ) diff --git a/agentic_security/static/main.js b/agentic_security/static/main.js index fbb4207..c60a6fd 100644 --- a/agentic_security/static/main.js +++ b/agentic_security/static/main.js @@ -71,6 +71,8 @@ var app = new Vue({ progressWidth: '0%', modelSpec: LLM_SPECS[0], budget: 50, + showParams: false, + optimize: false, showDatasets: false, scanResults: [], mainTable: [], @@ -237,7 +239,9 @@ var app = new Vue({ else if (strengthRate > 0) return 'text-red-500'; else return 'text-gray-100'; // This can be the default for strengthRate of 0 or less }, - + toggleParams() { + this.showParams = !this.showParams; + }, adjustHeight(event) { const element = event.target; // Reset height to ensure accurate measurement @@ -337,6 +341,7 @@ var app = new Vue({ maxBudget: this.budget, llmSpec: this.modelSpec, datasets: this.dataConfig, + optimize: this.optimize, }; const response = await fetch(`${URL}/scan`, { method: 'POST', diff --git a/poetry.lock b/poetry.lock index 6eec947..01de1f4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -998,6 +998,17 @@ toml = ">=0.10.2" types-toml = ">=0.10.8.7" typing-extensions = "*" +[[package]] +name = "joblib" +version = "1.4.2" +description = "Lightweight pipelining with Python functions" +optional = false +python-versions = ">=3.8" +files = [ + {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, + {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, +] + [[package]] name = "jsonpatch" version = "1.33" @@ -1862,6 +1873,23 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "pyaml" +version = "24.7.0" +description = "PyYAML-based module to produce a bit more pretty and readable YAML-serialized data" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyaml-24.7.0-py3-none-any.whl", hash = "sha256:6b06596cb5ac438a3fad1e1bf5775088c4d3afb927e2b03a29305d334835deb2"}, + {file = "pyaml-24.7.0.tar.gz", hash = "sha256:5d0fdf9e681036fb263a783d0298fc3af580a6e2a6cf1a3314ffc48dc3d91ccb"}, +] + +[package.dependencies] +PyYAML = "*" + +[package.extras] +anchors = ["unidecode"] + [[package]] name = "pyarrow" version = "17.0.0" @@ -2230,6 +2258,122 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "scikit-learn" +version = "1.5.1" +description = "A set of python modules for machine learning and data mining" +optional = false +python-versions = ">=3.9" +files = [ + {file = "scikit_learn-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:781586c414f8cc58e71da4f3d7af311e0505a683e112f2f62919e3019abd3745"}, + {file = "scikit_learn-1.5.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5b213bc29cc30a89a3130393b0e39c847a15d769d6e59539cd86b75d276b1a7"}, + {file = "scikit_learn-1.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ff4ba34c2abff5ec59c803ed1d97d61b036f659a17f55be102679e88f926fac"}, + {file = "scikit_learn-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:161808750c267b77b4a9603cf9c93579c7a74ba8486b1336034c2f1579546d21"}, + {file = "scikit_learn-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:10e49170691514a94bb2e03787aa921b82dbc507a4ea1f20fd95557862c98dc1"}, + {file = "scikit_learn-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:154297ee43c0b83af12464adeab378dee2d0a700ccd03979e2b821e7dd7cc1c2"}, + {file = "scikit_learn-1.5.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b5e865e9bd59396220de49cb4a57b17016256637c61b4c5cc81aaf16bc123bbe"}, + {file = "scikit_learn-1.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909144d50f367a513cee6090873ae582dba019cb3fca063b38054fa42704c3a4"}, + {file = "scikit_learn-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:689b6f74b2c880276e365fe84fe4f1befd6a774f016339c65655eaff12e10cbf"}, + {file = "scikit_learn-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:9a07f90846313a7639af6a019d849ff72baadfa4c74c778821ae0fad07b7275b"}, + {file = "scikit_learn-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5944ce1faada31c55fb2ba20a5346b88e36811aab504ccafb9f0339e9f780395"}, + {file = "scikit_learn-1.5.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0828673c5b520e879f2af6a9e99eee0eefea69a2188be1ca68a6121b809055c1"}, + {file = "scikit_learn-1.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:508907e5f81390e16d754e8815f7497e52139162fd69c4fdbd2dfa5d6cc88915"}, + {file = "scikit_learn-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97625f217c5c0c5d0505fa2af28ae424bd37949bb2f16ace3ff5f2f81fb4498b"}, + {file = "scikit_learn-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:da3f404e9e284d2b0a157e1b56b6566a34eb2798205cba35a211df3296ab7a74"}, + {file = "scikit_learn-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:88e0672c7ac21eb149d409c74cc29f1d611d5158175846e7a9c2427bd12b3956"}, + {file = "scikit_learn-1.5.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:7b073a27797a283187a4ef4ee149959defc350b46cbf63a84d8514fe16b69855"}, + {file = "scikit_learn-1.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b59e3e62d2be870e5c74af4e793293753565c7383ae82943b83383fdcf5cc5c1"}, + {file = "scikit_learn-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd8d3a19d4bd6dc5a7d4f358c8c3a60934dc058f363c34c0ac1e9e12a31421d"}, + {file = "scikit_learn-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:5f57428de0c900a98389c4a433d4a3cf89de979b3aa24d1c1d251802aa15e44d"}, + {file = "scikit_learn-1.5.1.tar.gz", hash = "sha256:0ea5d40c0e3951df445721927448755d3fe1d80833b0b7308ebff5d2a45e6414"}, +] + +[package.dependencies] +joblib = ">=1.2.0" +numpy = ">=1.19.5" +scipy = ">=1.6.0" +threadpoolctl = ">=3.1.0" + +[package.extras] +benchmark = ["matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "pandas (>=1.1.5)"] +build = ["cython (>=3.0.10)", "meson-python (>=0.16.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.23)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-gallery (>=0.16.0)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)"] +examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"] +install = ["joblib (>=1.2.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)", "threadpoolctl (>=3.1.0)"] +maintenance = ["conda-lock (==2.5.6)"] +tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.23)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.2.1)", "scikit-image (>=0.17.2)"] + +[[package]] +name = "scikit-optimize" +version = "0.9.0" +description = "Sequential model-based optimization toolbox." +optional = false +python-versions = "*" +files = [ + {file = "scikit-optimize-0.9.0.tar.gz", hash = "sha256:77d8c9e64947fc9f5cc05bbc6aed7b8a9907871ae26fe11997fd67be90f26008"}, + {file = "scikit_optimize-0.9.0-py2.py3-none-any.whl", hash = "sha256:5a439a18232381fad4bda78e914b616416720708e67f123498d14bd2842d861a"}, +] + +[package.dependencies] +joblib = ">=0.11" +numpy = ">=1.13.3" +pyaml = ">=16.9" +scikit-learn = ">=0.20.0" +scipy = ">=0.19.1" + +[package.extras] +plots = ["matplotlib (>=2.0.0)"] + +[[package]] +name = "scipy" +version = "1.14.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69"}, + {file = "scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad"}, + {file = "scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2"}, + {file = "scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2"}, + {file = "scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066"}, + {file = "scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1"}, + {file = "scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e"}, + {file = "scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06"}, + {file = "scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84"}, + {file = "scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417"}, +] + +[package.dependencies] +numpy = ">=1.23.5,<2.3" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + [[package]] name = "six" version = "1.16.0" @@ -2312,6 +2456,17 @@ files = [ [package.extras] tests = ["pytest", "pytest-cov"] +[[package]] +name = "threadpoolctl" +version = "3.5.0" +description = "threadpoolctl" +optional = false +python-versions = ">=3.8" +files = [ + {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"}, + {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, +] + [[package]] name = "toml" version = "0.10.2" @@ -2680,4 +2835,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "e39fddc2fec3061cdacb0c6494274b8fa6144005e6f97426658319d1713990a7" +content-hash = "814da9c38526493336e99b023168ac5ec3bf08154c0d5c3739f165b2476c8647" diff --git a/pyproject.toml b/pyproject.toml index cbc7ff1..6f8e014 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ tabulate = ">=0.8.9,<0.10.0" colorama = "^0.4.4" matplotlib = "^3.9.2" pydantic = "2.8.2" +scikit-optimize = "^0.9.0" [tool.poetry.group.dev.dependencies] black = "^24.8.0"