Compare commits

...

90 Commits

Author SHA1 Message Date
codebeaver-ai[bot] 317fd33480 Adding codebeaver.yml 2025-03-09 14:42:33 +00:00
codebeaver-ai[bot] 56e3c91af2 test: Add coverage improvement test for tests/test_scan.py 2025-03-09 14:42:32 +00:00
codebeaver-ai[bot] 594f8960e8 test: Add coverage improvement test for tests/test_report_chart.py 2025-03-09 14:42:30 +00:00
codebeaver-ai[bot] 51a9b5de5f test: Add coverage improvement test for agentic_security/probe_actor/test_refusal.py 2025-03-09 14:42:28 +00:00
codebeaver-ai[bot] 0a555b8427 test: Update coverage improvement test for agentic_security/core/test_app.py 2025-03-09 14:42:27 +00:00
Alexander Myasoedov aa27817f94 Merge pull request #164 from Praveenk8051/feat/parallel-tests-execution-pydist
chore: update pytest configuration to enable parallel test execution
2025-03-09 16:41:43 +02:00
Praveenk8051 8bd76b9f05 chore: update pytest configuration to enable parallel test execution 2025-03-09 14:15:44 +01:00
Praveenk8051 6f3c522d59 chore: update poetry.lock and pyproject.toml to include pytest-xdist 2025-03-09 14:01:50 +01:00
Alexander Myasoedov 896ca95ae2 Merge pull request #161 from dogakantarci/move-tests-from-routes-to-tests-div
fix: move routes/test_* to /tests/routes
2025-03-09 12:32:44 +02:00
doa f85c77d622 fix: move routes/test_* to /tests/routes 2025-03-08 23:46:09 +03:00
Alexander Myasoedov 684ba0b70d Merge pull request #160 from dogakantarci/move-test-files
Moved test files from agentic_security.probe_actor to tests.probe_actor
2025-03-08 20:00:18 +02:00
doa 21b43b18e7 Moved test files from agentic_security.probe_actor to tests.probe_actor 2025-03-08 20:18:54 +03:00
Alexander Myasoedov d20c1a3d0d Merge pull request #159 from dogakantarci/move-test-model
fix: issue #156 - move test_model.py file to /tests directory
2025-03-08 18:10:06 +02:00
doa ebac62e21a fix: issue #156 - move test file to tests dir 2025-03-08 19:01:57 +03:00
Alexander Myasoedov 21180b53e5 feat(bump version): 2025-03-07 19:42:59 +02:00
Alexander Myasoedov a8808b3165 Merge pull request #152 from msoedov/dependabot/pip/mkdocstrings-0.28.2
build(deps-dev): bump mkdocstrings from 0.28.1 to 0.28.2
2025-03-07 19:35:13 +02:00
Alexander Myasoedov 87c26ca3cc Merge pull request #153 from msoedov/dependabot/pip/numpy-2.2.3
build(deps): bump numpy from 2.2.2 to 2.2.3
2025-03-07 19:35:04 +02:00
dependabot[bot] e06c6932de build(deps): bump numpy from 2.2.2 to 2.2.3
Bumps [numpy](https://github.com/numpy/numpy) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.2.2...v2.2.3)

---
updated-dependencies:
- dependency-name: numpy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 17:23:41 +00:00
dependabot[bot] 51fcc38885 build(deps-dev): bump mkdocstrings from 0.28.1 to 0.28.2
Bumps [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings) from 0.28.1 to 0.28.2.
- [Release notes](https://github.com/mkdocstrings/mkdocstrings/releases)
- [Changelog](https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.1...0.28.2)

---
updated-dependencies:
- dependency-name: mkdocstrings
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 17:23:09 +00:00
Alexander Myasoedov 06a7bbfd87 Merge pull request #151 from msoedov/dependabot/pip/pytest-8.3.5
build(deps-dev): bump pytest from 8.3.4 to 8.3.5
2025-03-06 23:12:28 +02:00
Alexander Myasoedov 50f3e52445 Merge pull request #150 from msoedov/dependabot/pip/jinja2-3.1.6
build(deps): bump jinja2 from 3.1.5 to 3.1.6
2025-03-06 23:12:21 +02:00
dependabot[bot] 2bd62c21be build(deps-dev): bump pytest from 8.3.4 to 8.3.5
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.4 to 8.3.5.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.4...8.3.5)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 17:52:36 +00:00
dependabot[bot] d5d5dd48aa build(deps): bump jinja2 from 3.1.5 to 3.1.6
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.5 to 3.1.6.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.5...3.1.6)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 17:52:09 +00:00
Alexander Myasoedov bb2437197a Merge branch 'main' of github.com:msoedov/agentic_security 2025-03-05 19:39:08 +02:00
Alexander Myasoedov 51bb79aa6d feat(update docs): 2025-03-05 19:27:44 +02:00
Alexander Myasoedov 94f034fa9f Merge pull request #147 from msoedov/dependabot/pip/mkdocs-material-9.6.7
build(deps-dev): bump mkdocs-material from 9.6.4 to 9.6.7
2025-03-05 19:26:24 +02:00
Alexander Myasoedov f69de8720b Merge pull request #148 from msoedov/dependabot/pip/matplotlib-3.10.1
build(deps): bump matplotlib from 3.10.0 to 3.10.1
2025-03-05 19:26:14 +02:00
Alexander Myasoedov 7c9d83b1a7 fix(icon): 2025-03-05 19:25:45 +02:00
dependabot[bot] a9d4d671ba build(deps): bump matplotlib from 3.10.0 to 3.10.1
Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.10.0 to 3.10.1.
- [Release notes](https://github.com/matplotlib/matplotlib/releases)
- [Commits](https://github.com/matplotlib/matplotlib/compare/v3.10.0...v3.10.1)

---
updated-dependencies:
- dependency-name: matplotlib
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 17:18:42 +00:00
dependabot[bot] 554a219535 build(deps-dev): bump mkdocs-material from 9.6.4 to 9.6.7
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.4 to 9.6.7.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.4...9.6.7)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 17:18:14 +00:00
Alexander Myasoedov 32e99006bf feat(update doc style): 2025-03-05 19:16:43 +02:00
Alexander Myasoedov 8c09d65687 fix(test 2nd link): 2025-03-05 18:56:21 +02:00
Alexander Myasoedov a2842755fa fix(revert demo): 2025-03-05 18:55:34 +02:00
Alexander Myasoedov b923f7fea5 fix(readme): 2025-03-05 18:53:17 +02:00
Alexander Myasoedov 7f30a8ff7a Merge branch 'main' of github.com:msoedov/agentic_security 2025-03-05 18:49:00 +02:00
Alexander Myasoedov 909cbd69b4 feat(Readme update): 2025-03-05 18:48:52 +02:00
Alexander Myasoedov 4f0ebf180e Merge pull request #128 from msoedov/dependabot/pip/mypy-1.15.0
build(deps-dev): bump mypy from 1.14.1 to 1.15.0
2025-03-05 15:13:35 +02:00
dependabot[bot] 6be9673aa7 build(deps-dev): bump mypy from 1.14.1 to 1.15.0
Bumps [mypy](https://github.com/python/mypy) from 1.14.1 to 1.15.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.14.1...v1.15.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 13:08:33 +00:00
Alexander Myasoedov bd9ed97d85 Merge pull request #146 from msoedov/dependabot/pip/termcolor-2.5.0
build(deps): bump termcolor from 2.4.0 to 2.5.0
2025-03-05 15:06:40 +02:00
dependabot[bot] 3c88a4d6ba build(deps): bump termcolor from 2.4.0 to 2.5.0
Bumps [termcolor](https://github.com/termcolor/termcolor) from 2.4.0 to 2.5.0.
- [Release notes](https://github.com/termcolor/termcolor/releases)
- [Changelog](https://github.com/termcolor/termcolor/blob/main/CHANGES.md)
- [Commits](https://github.com/termcolor/termcolor/compare/2.4.0...2.5.0)

---
updated-dependencies:
- dependency-name: termcolor
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 17:36:34 +00:00
Alexander Myasoedov 2001eeb125 fix(init_banner): 2025-03-04 17:30:10 +02:00
Alexander Myasoedov a26b5dd448 feat(error handling in fuzzer): 2025-03-04 09:34:24 +02:00
Alexander Myasoedov 716a0f67f3 Merge branch 'main' of github.com:msoedov/agentic_security 2025-03-04 09:29:24 +02:00
Alexander Myasoedov c1bbf6b422 fix(error handling on verify): 2025-03-04 09:24:19 +02:00
Alexander Myasoedov 705fe21887 Merge pull request #144 from msoedov/dependabot/pip/fastapi-0.115.11
build(deps): bump fastapi from 0.115.8 to 0.115.11
2025-03-03 20:03:34 +02:00
dependabot[bot] 6505d29d36 build(deps): bump fastapi from 0.115.8 to 0.115.11
Bumps [fastapi](https://github.com/fastapi/fastapi) from 0.115.8 to 0.115.11.
- [Release notes](https://github.com/fastapi/fastapi/releases)
- [Commits](https://github.com/fastapi/fastapi/compare/0.115.8...0.115.11)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 17:58:08 +00:00
Alexander Myasoedov 801a330e27 feat(add fe is_active logic): 2025-03-02 22:55:21 +02:00
Alexander Myasoedov 92cabf6483 feat(add is_active flag): 2025-03-02 22:47:12 +02:00
Alexander Myasoedov 38f1bd7450 fix(pc): 2025-03-02 20:12:12 +02:00
Alexander Myasoedov ead883eeed feat(add test_registry_accessibility): 2025-03-02 20:09:03 +02:00
Alexander Myasoedov 5a57b997e5 fix(pc): 2025-03-02 19:57:48 +02:00
Alexander Myasoedov a8516a2da3 refactor(deprecate models.schema): 2025-03-02 19:57:09 +02:00
Alexander Myasoedov cb3a9bcbc0 fix(pc): 2025-03-02 19:48:45 +02:00
Alexander Myasoedov 3b2f407f2d fix(fuzzer): 2025-03-02 19:46:32 +02:00
Alexander Myasoedov 4b0ecc70ca fix(fuzzer): 2025-03-02 19:41:10 +02:00
Alexander Myasoedov 59d77904dd feat(add process_prompt_batch): 2025-03-02 19:35:05 +02:00
Alexander Myasoedov a8dd608f06 feat(add t5 model for testing): 2025-03-02 19:13:48 +02:00
Alexander Myasoedov f8102d1ee9 refactor(generate_default_settings): 2025-03-02 19:05:23 +02:00
Alexander Myasoedov ad6e0dbbc8 feat(move banner): 2025-03-02 19:04:17 +02:00
Alexander Myasoedov 6a8cc9bb14 feat(update test markers): 2025-03-02 19:04:08 +02:00
Alexander Myasoedov 263a282f47 feat(update pytest options): 2025-03-02 19:03:53 +02:00
Alexander Myasoedov 181e39bcfb feat(add slow test pytest cfg): 2025-03-02 19:03:39 +02:00
Alexander Myasoedov ec4bb0b086 feat(add tests dir): 2025-03-02 18:37:38 +02:00
Alexander Myasoedov cfd621bd4f Merge pull request #141 from msoedov/dependabot/pip/inline-snapshot-0.20.3
build(deps-dev): bump inline-snapshot from 0.20.1 to 0.20.3
2025-02-28 20:25:54 +02:00
dependabot[bot] 072ce574ad build(deps-dev): bump inline-snapshot from 0.20.1 to 0.20.3
Bumps [inline-snapshot](https://github.com/15r10nk/inline-snapshot) from 0.20.1 to 0.20.3.
- [Release notes](https://github.com/15r10nk/inline-snapshot/releases)
- [Changelog](https://github.com/15r10nk/inline-snapshot/blob/main/CHANGELOG.md)
- [Commits](https://github.com/15r10nk/inline-snapshot/compare/0.20.1...0.20.3)

---
updated-dependencies:
- dependency-name: inline-snapshot
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-28 18:11:00 +00:00
Alexander Myasoedov a63106686f feat(Add banner): 2025-02-28 19:32:40 +02:00
Alexander Myasoedov 3d14cc3719 Merge pull request #140 from arvinnick/issue-138
caught the json exception and imitated another error which was above it
2025-02-25 12:32:06 +02:00
arvinnick b152e78de3 caught the json exception and imitated another error which was above it 2025-02-25 10:15:24 +04:00
Alexander Myasoedov 7e458dbfc4 fix(ignore_errors = [KeyboardInterrupt]): 2025-02-24 19:55:12 +02:00
Alexander Myasoedov e12ef2d0db fix(linter): 2025-02-22 12:14:01 +02:00
Alexander Myasoedov ce3686e198 feat(add crew agent): 2025-02-22 12:13:43 +02:00
Alexander Myasoedov c79172b4df feat(add pydantic_ai): 2025-02-22 12:13:31 +02:00
Alexander Myasoedov e26d4ab841 feat(add agents module): 2025-02-22 12:13:18 +02:00
Alexander Myasoedov a377e82a24 fix(update operator py): 2025-02-21 23:06:04 +02:00
Alexander Myasoedov 126bf11b63 feat(add orjson): 2025-02-21 19:58:06 +02:00
Alexander Myasoedov 4b0b6987cb feat(add configurable network timeout): 2025-02-21 19:30:03 +02:00
Alexander Myasoedov 0ce4aac682 feat(add caching cfg): 2025-02-21 19:25:24 +02:00
Alexander Myasoedov c15ac38bec feat(add network.retry): 2025-02-21 19:17:45 +02:00
Alexander Myasoedov bf14877ef4 refactor(config->settings): 2025-02-21 19:16:51 +02:00
Alexander Myasoedov b8069b809a fix(E( 00.00 )): 2025-02-21 19:02:45 +02:00
Alexander Myasoedov 5c37e33069 feat( FastAPI(default_response_class=ORJSONResponse)): 2025-02-21 18:57:03 +02:00
Alexander Myasoedov 5bb5fafa89 Merge pull request #136 from maystrenk0/maystrenk0
fix(add spec OpenRouter.ai to base.js)
2025-02-21 15:34:07 +02:00
Oleksandr Maistrenko be85b21767 fix(add spec OpenRouter.ai to base.js) 2025-02-21 13:36:55 +02:00
Alexander Myasoedov 7e05716977 fix(typo): 2025-02-21 00:02:07 +02:00
Alexander Myasoedov 518cbf7fc3 feat(Update readme): 2025-02-21 00:01:10 +02:00
Alexander Myasoedov 1fdc1eb8de feat(fix linter): 2025-02-20 23:35:12 +02:00
Alexander Myasoedov ba67dd40ff fix(typo): 2025-02-20 23:32:58 +02:00
Alexander Myasoedov 3c75a24622 fix(fmt): 2025-02-20 23:31:25 +02:00
Alexander Myasoedov 60e6dd0a1a fix(empty value in secret expansion): 2025-02-20 23:31:06 +02:00
Alexander Myasoedov c97e43612b fix(linter): 2025-02-20 23:29:46 +02:00
52 changed files with 2044 additions and 691 deletions
+1
View File
@@ -17,3 +17,4 @@ inv/
scripts/
docx/
agentic_security.toml
/venv
+52 -24
View File
@@ -1,37 +1,54 @@
<p align="center">
<h1 align="center">Agentic Security</h1>
<p align="center">
The open-source Agentic LLM Vulnerability Scanner
<br />
<br />
<h1 align="center">Agentic Security</h1>
<p align="center">
An open-source vulnerability scanner for Agent Workflows and Large Language Models (LLMs)<br />
Protecting AI systems from jailbreaks, fuzzing, and multimodal attacks.<br />
<a href="https://agentic-security.vercel.app">Explore the docs »</a> ·
<a href="https://github.com/msoedov/agentic_security/issues">Report a Bug »</a>
</p>
</p>
<p align="center">
<a href="https://github.com/msoedov/agentic_security/commits/main">
<img alt="GitHub Last Commit" src="https://img.shields.io/github/last-commit/msoedov/agentic_security?style=for-the-badge&logo=git&labelColor=000000&logoColor=FFFFFF&label=Last Commit&color=6A35FF" />
<img alt="GitHub Last Commit" src="https://img.shields.io/github/last-commit/msoedov/agentic_security?style=for-the-badge&logo=git&labelColor=000000&color=6A35FF" />
</a>
<a href="https://github.com/msoedov/agentic_security">
<img alt="GitHub Repo Size" src="https://img.shields.io/github/repo-size/msoedov/agentic_security?style=for-the-badge&logo=database&labelColor=000000&logoColor=FFFFFF&label=Repo Size&color=yellow" />
</a>
<img alt="GitHub Repo Size" src="https://img.shields.io/github/repo-size/msoedov/agentic_security?style=for-the-badge&logo=database&labelColor=000000&color=yellow" />
</a>
<a href="https://github.com/msoedov/agentic_security/blob/master/LICENSE">
<img alt="GitHub License" src="https://img.shields.io/github/license/msoedov/agentic_security?style=for-the-badge&logo=codeigniter&labelColor=000000&logoColor=FFFFFF&label=License&color=FFCC19" />
<img alt="GitHub License" src="https://img.shields.io/github/license/msoedov/agentic_security?style=for-the-badge&logo=codeigniter&labelColor=000000&color=FFCC19" />
</a>
<a href="https://pypi.org/project/agentic-security/">
<img alt="PyPI Version" src="https://img.shields.io/pypi/v/agentic-security?style=for-the-badge&logo=pypi&labelColor=000000&color=00CCFF" />
</a>
<a href="https://discord.gg/stw3DfZQ">
<img alt="Join Discord" src="https://img.shields.io/badge/Discord-Join%20Us-black?style=for-the-badge&logo=discord&labelColor=000000&color=DD55FF" />
</a>
<a href="https://discord.gg/stw3DfZQ"><img alt="Join the community" src="https://img.shields.io/badge/Join%20the%20community-black.svg?style=for-the-badge&logo=lightning&labelColor=000000&logoColor=FFFFFF&label=&color=DD55FF&logoWidth=20" /></a>
</p>
## Features
- Multi modal attacks and vulnerability scanners🛠️
- Multi-Step/multi-round Jailbreaks 🌀
- Comprehensive fuzzing for any LLMs 🧪
- LLM API integration and stress testing 🛠️
- RL based attacks 📡
Note: Please be aware that Agentic Security is designed as a safety scanner tool and not a foolproof solution. It cannot guarantee complete protection against all possible threats.
Agentic Security equips you with powerful tools to safeguard LLMs against emerging threats. Here's what you can do:
- **Multimodal Attacks** 🖼️🎙️
Probe vulnerabilities across text, images, and audio inputs to ensure your LLM is robust against diverse threats.
- **Multi-Step Jailbreaks** 🌀
Simulate sophisticated, iterative attack sequences to uncover weaknesses in LLM safety mechanisms.
- **Comprehensive Fuzzing** 🧪
Stress-test any LLM with randomized inputs to identify edge cases and unexpected behaviors.
- **API Integration & Stress Testing** 🌐
Seamlessly connect to LLM APIs and push their limits with high-volume, real-world attack scenarios.
- **RL-Based Attacks** 📡
Leverage reinforcement learning to craft adaptive, intelligent probes that evolve with your models defenses.
> **Why It Matters**: These features help developers, researchers, and security teams proactively identify and mitigate risks in AI systems, ensuring safer and more reliable deployments.
## 📦 Installation
@@ -67,6 +84,7 @@ agentic_security --port=PORT --host=HOST
## UI 🧙
<img width="100%" alt="booking-screen" src="https://res.cloudinary.com/dq0w2rtm9/image/upload/v1736433557/z0bsyzhsqlgcr3w4ovwp.gif">
<img width="100%" alt="booking-screen" src="https://res.cloudinary.com/dq0w2rtm9/image/upload/v1741192668/final_aa9jhb.gif">
## LLM kwargs
@@ -111,7 +129,7 @@ Init config
```shell
agentic_security init
2025-01-08 20:12:02.449 | INFO | agentic_security.lib:generate_default_cfg:324 - Default configuration generated successfully to agesec.toml.
2025-01-08 20:12:02.449 | INFO | agentic_security.lib:generate_default_settings:324 - Default configuration generated successfully to agesec.toml.
```
@@ -391,10 +409,15 @@ For more detailed information on how to use Agentic Security, including advanced
## Roadmap and Future Goals
- \[ \] Expand dataset variety
- \[ \] Introduce two new attack vectors
- \[ \] Develop initial attacker LLM
- \[ \] Complete integration of OWASP Top 10 classification
Were just getting started! Heres whats on the horizon:
- **RL-Powered Attacks**: An attacker LLM trained with reinforcement learning to dynamically evolve jailbreaks and outsmart defenses.
- **Massive Dataset Expansion**: Scaling to 100,000+ prompts across text, image, and audio modalities—curated for real-world threats.
- **Daily Attack Updates**: Fresh attack vectors delivered daily, keeping your scans ahead of the curve.
- **Community Modules**: A plug-and-play ecosystem where you can share and deploy custom probes, datasets, and integrations.
| Tool | Source | Integrated |
|-------------------------|-------------------------------------------------------------------------------|------------|
@@ -422,4 +445,9 @@ Before contributing, please read the contributing guidelines.
Agentic Security is released under the Apache License v2.
## 🚫 No Cryptocurrency Affiliation
Agentic Security is focused solely on AI security and has no affiliation with cryptocurrency projects, blockchain technologies, or related initiatives. Our mission is to advance the safety and reliability of AI systems—no tokens, no coins, just code.
## Contact us
+3 -1
View File
@@ -6,6 +6,7 @@ import uvicorn
from agentic_security.app import app
from agentic_security.lib import AgenticSecurity
from agentic_security.misc.banner import init_banner
class CLI:
@@ -38,7 +39,7 @@ class CLI:
Generate the default CI configuration file.
"""
sys.path.append(os.path.dirname("."))
AgenticSecurity().generate_default_cfg(host, port)
AgenticSecurity().generate_default_settings(host, port)
i = init
@@ -61,4 +62,5 @@ def main():
if __name__ == "__main__":
init_banner()
main()
View File
+256
View File
@@ -0,0 +1,256 @@
import asyncio
import logging
import os
from typing import Any
import httpx
from crewai import Agent, Crew, Task
from crewai_tools import tool
from pydantic import BaseModel, ConfigDict, Field
# Assuming LLMSpec is defined elsewhere; placeholder import
from agentic_security.http_spec import LLMSpec
LLM_SPECS = [] # Populate with LLM spec strings if needed
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Define AgentSpecification model
class AgentSpecification(BaseModel):
name: str | None = Field(None, description="Name of the LLM/agent")
version: str | None = Field(None, description="Version of the LLM/agent")
description: str | None = Field(None, description="Description of the LLM/agent")
capabilities: list[str] | None = Field(None, description="List of capabilities")
configuration: dict[str, Any] | None = Field(
None, description="Configuration settings"
)
endpoint: str | None = Field(None, description="Endpoint URL of the deployed agent")
model_config = ConfigDict(arbitrary_types_allowed=True)
# Define OperatorToolBox class (unchanged from original)
class OperatorToolBox:
def __init__(self, spec: AgentSpecification, datasets: list[dict[str, Any]]):
self.spec = spec
self.datasets = datasets
self.failures = []
self.llm_specs = [LLMSpec.from_string(spec) for spec in LLM_SPECS]
def get_spec(self) -> AgentSpecification:
return self.spec
def get_datasets(self) -> list[dict[str, Any]]:
return self.datasets
def validate(self) -> bool:
if not self.spec.name or not self.spec.version:
self.failures.append("Invalid specification: Name or version is missing.")
return False
if not self.datasets:
self.failures.append("No datasets provided.")
return False
return True
def stop(self) -> None:
logger.info("Stopping the toolbox...")
def run(self) -> None:
logger.info("Running the toolbox...")
def get_results(self) -> list[dict[str, Any]]:
return self.datasets
def get_failures(self) -> list[str]:
return self.failures
def run_operation(self, operation: str) -> str:
if operation not in ["dataset1", "dataset2", "dataset3"]:
self.failures.append(f"Operation '{operation}' failed: Dataset not found.")
return f"Operation '{operation}' failed: Dataset not found."
return f"Operation '{operation}' executed successfully."
async def test_llm_spec(self, llm_spec: LLMSpec, user_prompt: str) -> str:
try:
response = await llm_spec.verify()
response.raise_for_status()
logger.info(f"Verification succeeded for {llm_spec.url}")
test_response = await llm_spec.probe(user_prompt)
test_response.raise_for_status()
response_data = test_response.json()
return f"Test succeeded for {llm_spec.url}: {response_data}"
except httpx.HTTPStatusError as e:
self.failures.append(f"HTTP error occurred: {e}")
logger.error(f"Test failed for {llm_spec.url}: {e}")
return f"Test failed for {llm_spec.url}: {e}"
except Exception as e:
self.failures.append(f"An error occurred: {e}")
logger.error(f"Test failed for {llm_spec.url}: {e}")
return f"Test failed for {llm_spec.url}: {e}"
async def test_with_prompt(self, spec_index: int, user_prompt: str) -> str:
if not 0 <= spec_index < len(self.llm_specs):
return f"Invalid spec index: {spec_index}. Valid range is 0 to {len(self.llm_specs) - 1}"
llm_spec = self.llm_specs[spec_index]
return await self.test_llm_spec(llm_spec, user_prompt)
# Define CrewAI Tools
@tool("validate_toolbox")
def validate_toolbox(toolbox: OperatorToolBox) -> str:
"""Validate the toolbox configuration."""
is_valid = toolbox.validate()
return (
"ToolBox validation successful." if is_valid else "ToolBox validation failed."
)
@tool("execute_operation")
def execute_operation(toolbox: OperatorToolBox, operation: str) -> str:
"""Execute a dataset operation."""
return toolbox.run_operation(operation)
@tool("retrieve_results")
def retrieve_results(toolbox: OperatorToolBox) -> str:
"""Retrieve the results of operations."""
results = toolbox.get_results()
return (
f"Operation Results:\n{results}"
if results
else "No operations have been executed yet."
)
@tool("retrieve_failures")
def retrieve_failures(toolbox: OperatorToolBox) -> str:
"""Retrieve recorded failures."""
failures = toolbox.get_failures()
return f"Failures:\n{failures}" if failures else "No failures recorded."
@tool("list_llm_specs")
def list_llm_specs(toolbox: OperatorToolBox) -> str:
"""List available LLM specifications."""
spec_list = "\n".join(
f"{i}: {spec.url}" for i, spec in enumerate(toolbox.llm_specs)
)
return f"Available LLM Specs:\n{spec_list}"
@tool("test_llm_with_prompt")
async def test_llm_with_prompt(
toolbox: OperatorToolBox, spec_index: int, user_prompt: str
) -> str:
"""Test an LLM spec with a user prompt."""
return await toolbox.test_with_prompt(spec_index, user_prompt)
# Setup OperatorToolBox
spec = AgentSpecification(
name="DeepSeek Chat",
version="1.0",
description="A powerful language model",
capabilities=["text-generation", "question-answering"],
configuration={"max_tokens": 100},
)
toolbox = OperatorToolBox(
spec=spec, datasets=[{"id": "dataset1"}, {"id": "dataset2"}, {"id": "dataset3"}]
)
# Define CrewAI Agent
dataset_manager_agent = Agent(
role="Dataset Manager",
goal="Manage and operate the OperatorToolBox to validate configurations, run operations, and test LLMs.",
backstory="An expert in dataset management and LLM testing, designed to assist with toolbox operations.",
verbose=True,
llm="openai", # Using OpenAI-compatible API for DeepSeek; adjust if DeepSeek has a specific ID
tools=[
validate_toolbox,
execute_operation,
retrieve_results,
retrieve_failures,
list_llm_specs,
test_llm_with_prompt,
],
allow_delegation=False, # Single agent, no delegation needed
)
# Define Tasks
tasks = [
Task(
description="Validate the toolbox configuration.",
agent=dataset_manager_agent,
expected_output="A string indicating whether validation succeeded or failed.",
),
Task(
description="List available LLM specifications.",
agent=dataset_manager_agent,
expected_output="A string listing available LLM specs.",
),
Task(
description="Guide the user to test an LLM with the prompt: 'Tell me a short story about a robot'. Suggest listing specs first.",
agent=dataset_manager_agent,
expected_output="A string suggesting the user list specs and proceed with testing.",
),
]
# Define Crew
crew = Crew(
agents=[dataset_manager_agent],
tasks=tasks,
verbose=2, # Detailed logging
)
# Async wrapper to handle async tools
async def run_crew():
# Since CrewAI's process() is synchronous but our tool is async, we need to run it in an event loop
result = (
crew.kickoff()
) # Synchronous call; async tools are awaited internally by CrewAI
print("\nCrew Results:")
for task_result in result:
print(f"Task: {task_result.description}")
print(f"Output: {task_result.output}\n")
# Handle user interaction for LLM testing
print("Please select a spec index from the listed specs and confirm to proceed.")
user_input = (
input("Enter spec index and 'yes' to confirm (e.g., '0 yes'): ").strip().split()
)
if len(user_input) == 2 and user_input[1].lower() == "yes":
try:
spec_index = int(user_input[0])
user_prompt = "Tell me a short story about a robot"
# Create a new task for testing
test_task = Task(
description=f"Test LLM at index {spec_index} with prompt: '{user_prompt}'",
agent=dataset_manager_agent,
expected_output="A string with the test result from the LLM.",
)
test_crew = Crew(
agents=[dataset_manager_agent], tasks=[test_task], verbose=2
)
test_result = test_crew.kickoff()
print(f"Test Output: {test_result[0].output}\n")
except ValueError:
print("Invalid spec index provided.\n")
else:
print("Test canceled. Please provide a valid index and confirmation.\n")
# Ensure DeepSeek API key is set
os.environ["OPENAI_API_KEY"] = os.environ.get(
"DEEPSEEK_API_KEY", ""
) # CrewAI uses OPENAI_API_KEY
os.environ[
"OPENAI_MODEL_NAME"
] = "deepseek:chat" # Specify DeepSeek model (adjust if needed)
if __name__ == "__main__":
asyncio.run(run_crew())
@@ -0,0 +1,238 @@
import asyncio
import logging
from typing import Any
import httpx
from pydantic import BaseModel, ConfigDict, Field
from pydantic_ai import Agent, RunContext, Tool
# Assuming LLMSpec is defined elsewhere; placeholder import
from agentic_security.http_spec import LLMSpec
LLM_SPECS = [] # Populate this list with LLM spec strings if needed
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Define AgentSpecification model
class AgentSpecification(BaseModel):
name: str | None = Field(None, description="Name of the LLM/agent")
version: str | None = Field(None, description="Version of the LLM/agent")
description: str | None = Field(None, description="Description of the LLM/agent")
capabilities: list[str] | None = Field(None, description="List of capabilities")
configuration: dict[str, Any] | None = Field(
None, description="Configuration settings"
)
endpoint: str | None = Field(None, description="Endpoint URL of the deployed agent")
model_config = ConfigDict(arbitrary_types_allowed=True)
# Define OperatorToolBox class
class OperatorToolBox:
def __init__(self, spec: AgentSpecification, datasets: list[dict[str, Any]]):
self.spec = spec
self.datasets = datasets
self.failures = []
self.llm_specs = [LLMSpec.from_string(spec) for spec in LLM_SPECS]
def get_spec(self) -> AgentSpecification:
return self.spec
def get_datasets(self) -> list[dict[str, Any]]:
return self.datasets
def validate(self) -> bool:
if not self.spec.name or not self.spec.version:
self.failures.append("Invalid specification: Name or version is missing.")
return False
if not self.datasets:
self.failures.append("No datasets provided.")
return False
return True
def stop(self) -> None:
logger.info("Stopping the toolbox...")
def run(self) -> None:
logger.info("Running the toolbox...")
def get_results(self) -> list[dict[str, Any]]:
return self.datasets
def get_failures(self) -> list[str]:
return self.failures
def run_operation(self, operation: str) -> str:
if operation not in ["dataset1", "dataset2", "dataset3"]:
self.failures.append(f"Operation '{operation}' failed: Dataset not found.")
return f"Operation '{operation}' failed: Dataset not found."
return f"Operation '{operation}' executed successfully."
async def test_llm_spec(self, llm_spec: LLMSpec, user_prompt: str) -> str:
try:
response = await llm_spec.verify()
response.raise_for_status()
logger.info(f"Verification succeeded for {llm_spec.url}")
test_response = await llm_spec.probe(user_prompt)
test_response.raise_for_status()
response_data = test_response.json()
return f"Test succeeded for {llm_spec.url}: {response_data}"
except httpx.HTTPStatusError as e:
self.failures.append(f"HTTP error occurred: {e}")
logger.error(f"Test failed for {llm_spec.url}: {e}")
return f"Test failed for {llm_spec.url}: {e}"
except Exception as e:
self.failures.append(f"An error occurred: {e}")
logger.error(f"Test failed for {llm_spec.url}: {e}")
return f"Test failed for {llm_spec.url}: {e}"
async def test_with_prompt(self, spec_index: int, user_prompt: str) -> str:
if not 0 <= spec_index < len(self.llm_specs):
return f"Invalid spec index: {spec_index}. Valid range is 0 to {len(self.llm_specs) - 1}"
llm_spec = self.llm_specs[spec_index]
return await self.test_llm_spec(llm_spec, user_prompt)
# Define the Agent
class DatasetManagerAgent(Agent):
model: str = "deepseek:chat"
system_prompt: str = (
"You are an AI agent managing an OperatorToolBox. You can validate the toolbox, run operations, "
"retrieve results or failures, list LLM specs, and test LLM specs with user prompts. "
"Use the provided tools to assist the user based on their request."
)
def __init__(self, toolbox: OperatorToolBox, **kwargs):
super().__init__(**kwargs)
self.toolbox = toolbox
# Define async tools within __init__
async def validate_toolbox(ctx: RunContext[Any]) -> str:
is_valid = self.toolbox.validate()
return (
"ToolBox validation successful."
if is_valid
else "ToolBox validation failed."
)
async def execute_operation(ctx: RunContext[Any], operation: str) -> str:
return self.toolbox.run_operation(operation)
async def retrieve_results(ctx: RunContext[Any]) -> str:
results = self.toolbox.get_results()
return (
f"Operation Results:\n{results}"
if results
else "No operations have been executed yet."
)
async def retrieve_failures(ctx: RunContext[Any]) -> str:
failures = self.toolbox.get_failures()
return f"Failures:\n{failures}" if failures else "No failures recorded."
async def list_llm_specs(ctx: RunContext[Any]) -> str:
spec_list = "\n".join(
f"{i}: {spec.url}" for i, spec in enumerate(self.toolbox.llm_specs)
)
return f"Available LLM Specs:\n{spec_list}"
async def test_llm_with_prompt(
ctx: RunContext[Any], spec_index: int, user_prompt: str
) -> str:
return await self.toolbox.test_with_prompt(spec_index, user_prompt)
# Register tools
self.tools = [
Tool(
name="validate_toolbox",
description="Validate the toolbox configuration.",
function=validate_toolbox,
),
Tool(
name="execute_operation",
description="Execute a dataset operation.",
function=execute_operation,
),
Tool(
name="retrieve_results",
description="Retrieve the results of operations.",
function=retrieve_results,
),
Tool(
name="retrieve_failures",
description="Retrieve recorded failures.",
function=retrieve_failures,
),
Tool(
name="list_llm_specs",
description="List available LLM specifications.",
function=list_llm_specs,
),
Tool(
name="test_llm_with_prompt",
description="Test an LLM spec with a user prompt.",
function=test_llm_with_prompt,
),
]
# Setup and run example
async def run_dataset_manager_agent_async():
# Initialize OperatorToolBox with AgentSpecification
spec = AgentSpecification(
name="DeepSeek Chat",
version="1.0",
description="A powerful language model",
capabilities=["text-generation", "question-answering"],
configuration={"max_tokens": 100},
)
toolbox = OperatorToolBox(
spec=spec, datasets=[{"id": "dataset1"}, {"id": "dataset2"}, {"id": "dataset3"}]
)
# Create the agent
agent = DatasetManagerAgent(toolbox=toolbox)
# Example prompts
prompts = [
"Validate the toolbox.",
"List available LLM specs.",
"I want to test an LLM with my prompt: 'Tell me a short story about a robot'. Which spec index should I use?",
]
for prompt in prompts:
result = await agent.run(prompt)
print(f"Prompt: {prompt}")
print(f"Response: {result}\n")
# Handle testing request
if "test an LLM with my prompt" in prompt:
print(
"Please select a spec index from the list above and confirm to proceed."
)
# Simulate user input (replace with real input in practice)
user_input = (
input("Enter spec index and 'yes' to confirm (e.g., '0 yes'): ")
.strip()
.split()
)
if len(user_input) == 2 and user_input[1].lower() == "yes":
try:
spec_index = int(user_input[0])
user_prompt = prompt.split("my prompt: ")[1].strip("'")
test_result = await agent.run(
f"Test LLM at index {spec_index} with prompt: {user_prompt}"
)
print(f"Test Response: {test_result}\n")
except ValueError:
print("Invalid spec index provided.\n")
else:
print("Test canceled. Please provide a valid index and confirmation.\n")
if __name__ == "__main__":
asyncio.run(run_dataset_manager_agent_async())
+37 -5
View File
@@ -1,16 +1,38 @@
from functools import lru_cache
import tomli
from loguru import logger
SETTINGS_VERSION = 1
class CfgMixin:
@lru_cache(maxsize=1)
def settings_var(name: str, default=None):
return get_or_create_config().get_config_value(name, default)
@lru_cache(maxsize=1)
def get_or_create_config():
cfg = SettingsMixin()
cfg.get_or_create_config()
return cfg
class SettingsMixin:
config = {}
default_path = "agentic_security.toml"
def get_or_create_config(self) -> bool:
if not self.has_local_config():
self.generate_default_cfg()
self.generate_default_settings()
return False
self.load_config(self.default_path)
settings_version = self.get_config_value("general.version")
if settings_version and settings_version != SETTINGS_VERSION:
logger.error(
f"Configuration version mismatch: expected {SETTINGS_VERSION}, got {settings_version}."
)
return False
return True
def has_local_config(self):
@@ -64,7 +86,7 @@ class CfgMixin:
return default
return value
def generate_default_cfg(self, host: str = "0.0.0.0", port: int = 8718):
def generate_default_settings(self, host: str = "0.0.0.0", port: int = 8718):
# Accept host / port as parameters
with open(self.default_path, "w") as f:
f.write(
@@ -84,6 +106,7 @@ maxBudget = 1000000 # Maximum budget for the scan
max_th = 0.3 # Maximum failure threshold (percentage)
optimize = false # Enable optimization during scanning
enableMultiStepAttack = false # Enable multi-step attack simulations
version = $SETTINGS_VERSION
# [modules.LLM-Jailbreak-Classifier]
# dataset_name = "markush1/LLM-Jailbreak-Classifier"
@@ -110,11 +133,20 @@ high = 0.5
OPENAI_API_KEY = "$OPENAI_API_KEY"
DEEPSEEK_API_KEY = "$DEEPSEEK_API_KEY"
[caching]
enable = true
cache_size = 10000
use_disk_cache = false
[network]
retry = 3
timeout_connect = 30
timeout_response = 90
""".replace(
"$HOST", host
).replace(
"$PORT", str(port)
)
.replace("$PORT", str(port))
.replace("$SETTINGS_VERSION", str(SETTINGS_VERSION))
)
logger.info(
+2 -1
View File
@@ -2,6 +2,7 @@ import os
from asyncio import Event, Queue
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
tools_inbox: Queue = Queue()
stop_event: Event = Event()
@@ -11,7 +12,7 @@ _secrets = {}
def create_app() -> FastAPI:
"""Create and configure the FastAPI application."""
app = FastAPI()
app = FastAPI(default_response_class=ORJSONResponse)
return app
+197
View File
@@ -1,15 +1,35 @@
import os
import pytest
from agentic_security.core.app import expand_secrets
@pytest.fixture(autouse=True)
def reset_globals():
"""
Reset globals (_secrets, current_run, tools_inbox, stop_event) before each test.
This ensures tests run in a clean state.
"""
from agentic_security.core.app import _secrets, current_run, get_tools_inbox, get_stop_event
_secrets.clear()
current_run["spec"] = ""
current_run["id"] = ""
# Clear tools_inbox queue
queue = get_tools_inbox()
while not queue.empty():
queue.get_nowait()
# Reset stop_event if it is set
event = get_stop_event()
if event.is_set():
event.clear()
def setup_env_vars():
# Set up environment variables for testing
os.environ["TEST_ENV_VAR"] = "test_value"
def test_expand_secrets_with_env_var():
os.environ["TEST_ENV_VAR"] = "test_value"
secrets = {"secret_key": "$TEST_ENV_VAR"}
expand_secrets(secrets)
assert secrets["secret_key"] == "test_value"
@@ -25,3 +45,180 @@ def test_expand_secrets_without_dollar_sign():
secrets = {"secret_key": "plain_value"}
expand_secrets(secrets)
assert secrets["secret_key"] == "plain_value"
import asyncio
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
from agentic_security.core.app import create_app, get_tools_inbox, get_stop_event, get_current_run, set_current_run, get_secrets, set_secrets, expand_secrets
class DummyLLMSpec:
"""A dummy LLMSpec for testing purposes."""
pass
def test_create_app():
"""Test that create_app returns a FastAPI app with ORJSONResponse."""
app = create_app()
assert isinstance(app, FastAPI)
assert app.router.default_response_class == ORJSONResponse
def test_get_tools_inbox():
"""Test that get_tools_inbox returns a Queue instance."""
queue = get_tools_inbox()
from asyncio import Queue
assert isinstance(queue, Queue)
def test_get_stop_event():
"""Test that get_stop_event returns an Event instance."""
event = get_stop_event()
from asyncio import Event
assert isinstance(event, Event)
def test_get_current_run_initial():
"""Test that get_current_run returns the initial current run dictionary."""
current = get_current_run()
# The initial dictionary should have an empty spec and id.
assert current["spec"] == ""
assert current["id"] == ""
def test_set_current_run():
"""Test that set_current_run updates the current run with the dummy LLMSpec."""
dummy_spec = DummyLLMSpec()
updated = set_current_run(dummy_spec)
assert updated["spec"] is dummy_spec
# Ensure that the id is computed as hash(id(dummy_spec))
expected_id = hash(id(dummy_spec))
assert updated["id"] == expected_id
def test_get_and_set_secrets():
"""Test that set_secrets updates the secrets dictionary and get_secrets returns the updated values."""
# Clear any previously set secrets
secrets_before = get_secrets().copy()
os.environ["MY_SECRET"] = "secret_value"
new_secrets = {"key1": "$MY_SECRET", "key2": "plain"}
updated = set_secrets(new_secrets)
assert updated["key1"] == "secret_value"
assert updated["key2"] == "plain"
def test_expand_secrets_multiple_keys():
"""Test expand_secrets with multiple keys, including one with an environment variable,
one with a non-existent variable, and one that is plain."""
os.environ["TEST_ENV_VAR"] = "test_value"
secrets = {"env_key": "$TEST_ENV_VAR", "nonexistent_key": "$NON_EXISTENT", "plain_key": "value"}
expand_secrets(secrets)
assert secrets["env_key"] == "test_value"
# For a non-existent environment variable, os.getenv returns None
assert secrets["nonexistent_key"] is None
# Plain values should not be changed.
assert secrets["plain_key"] == "value"
def test_expand_secrets_with_space_after_dollar():
"""Test expand_secrets when the value has a dollar sign followed by a space.
Since the value does not start strictly with "$", the secret remains unchanged.
Also verifies that the stripping in expand_secrets (via strip("$"))
will remove both dollar and any whitespace if the value actually started with '$'.
"""
os.environ["SPACED_VAR"] = "spaced_value"
secrets = {"key": "$ SPACED_VAR"}
expand_secrets(secrets)
# " $ SPACED_VAR" after strip("$") becomes " SPACED_VAR" which is not a valid env key so returns None.
assert secrets["key"] is None
def test_set_secrets_update_existing():
"""Test that set_secrets updates an existing secret and retains previously set keys."""
os.environ["VAR1"] = "value1"
os.environ["VAR2"] = "value2"
result_first = set_secrets({"a": "$VAR1", "b": "b_val"})
assert result_first["a"] == "value1"
# Change VAR1 in environment and update secret "a", and add secret "c"
os.environ["VAR1"] = "new_value1"
result_second = set_secrets({"a": "$VAR1", "c": "$VAR2"})
assert result_second["a"] == "new_value1"
assert result_second["b"] == "b_val"
assert result_second["c"] == "value2"
def test_tools_inbox_state():
"""Test that get_tools_inbox returns the same queue instance
and that the queue state persists across multiple calls.
"""
from asyncio import Queue
inbox1 = get_tools_inbox()
inbox1.put_nowait("message")
inbox2 = get_tools_inbox()
# inbox2 should contain the "message" from inbox1
msg = inbox2.get_nowait()
assert msg == "message"
def test_stop_event_state():
"""Test that stop_event can be set and cleared, and its state persists."""
event = get_stop_event()
# Initially the event should not be set
assert not event.is_set()
event.set()
assert event.is_set()
event.clear()
assert not event.is_set()
def test_set_current_run_returns_global_dict():
"""Test that set_current_run returns the same global current_run dictionary
as returned by get_current_run.
"""
dummy_spec = DummyLLMSpec()
updated = set_current_run(dummy_spec)
current = get_current_run()
assert updated is current
def test_get_secrets_initial():
"""Test that get_secrets returns an empty dictionary initially."""
assert get_secrets() == {}
def test_set_secrets_empty():
"""Test that setting an empty secrets dictionary does not modify existing secrets."""
# first set initial secrets
initial = {"key": "value"}
set_secrets(initial)
# update with an empty dict the existing keys remain
result = set_secrets({})
assert result == initial
def test_update_current_run_twice():
"""Test updating current run twice with different LLMSpec values."""
dummy1 = DummyLLMSpec()
dummy2 = DummyLLMSpec()
set_current_run(dummy1)
first = get_current_run().copy()
set_current_run(dummy2)
second = get_current_run().copy()
# first update should hold dummy1, second should hold dummy2
assert first["spec"] is dummy1
assert second["spec"] is dummy2
# Ensure that id has changed (using hash(id(dummy_spec)))
assert first["id"] != second["id"]
def test_expand_secrets_trailing_whitespace():
"""Test expand_secrets when the secret value has trailing whitespace after the dollar sign.
The trailing whitespace remains after stripping only the dollar sign, so the looked-up environment variable key will not match.
"""
os.environ["TRIM_TEST"] = "trimmed"
secrets = {"key": "$TRIM_TEST "}
expand_secrets(secrets)
# Since "TRIM_TEST " (with trailing space) is not set in the environment, the secret should be None.
assert secrets["key"] is None
def test_expand_secrets_empty_dict():
"""Test expand_secrets with an empty dictionary does nothing."""
secrets = {}
expand_secrets(secrets)
assert secrets == {}
def test_expand_secrets_with_non_string_value():
"""Test that expand_secrets raises an AttributeError when a secret value is not a string."""
secrets = {"key": 123}
with pytest.raises(AttributeError):
expand_secrets(secrets)
def test_expand_secrets_multiple_dollar_signs():
"""Test expand_secrets with a value that contains multiple leading dollar signs.
The extra dollar signs are removed by the strip method.
"""
os.environ["MULTI_DOLLAR_VAR"] = "multi_value"
secrets = {"key": "$$MULTI_DOLLAR_VAR"}
expand_secrets(secrets)
# After stripping, "$$MULTI_DOLLAR_VAR".strip("$") returns "MULTI_DOLLAR_VAR"
assert secrets["key"] == "multi_value"
+3 -5
View File
@@ -1,13 +1,11 @@
from agentic_security.config import CfgMixin
from agentic_security.config import get_or_create_config
from agentic_security.core.app import set_secrets
class InMemorySecrets:
def __init__(self):
self.secrets = {}
self.config = CfgMixin()
self.config.get_or_create_config()
self.secrets = self.config.config.get("secrets", {})
config = get_or_create_config()
self.secrets = config.get_config_value("secrets", {})
set_secrets(self.secrets)
def set_secret(self, key: str, value: str):
+18 -5
View File
@@ -4,6 +4,8 @@ from enum import Enum
import httpx
from pydantic import BaseModel
from agentic_security.config import settings_var
class Modality(Enum):
TEXT = 0
@@ -28,7 +30,7 @@ def encode_audio_base64_by_url(url: str) -> str:
class InvalidHTTPSpecError(Exception):
...
pass
class LLMSpec(BaseModel):
@@ -47,14 +49,21 @@ class LLMSpec(BaseModel):
except Exception as e:
raise InvalidHTTPSpecError(f"Failed to parse HTTP spec: {e}") from e
def timeout(self):
return (
settings_var("network.timeout_connect", 30),
settings_var("network.timeout_response", 90),
)
async def _probe_with_files(self, files):
async with httpx.AsyncClient() as client:
transport = httpx.AsyncHTTPTransport(retries=settings_var("network.retry", 3))
async with httpx.AsyncClient(transport=transport) as client:
response = await client.request(
method=self.method,
url=self.url,
headers=self.headers,
files=files,
timeout=(30, 90),
timeout=self.timeout(),
)
return response
@@ -90,13 +99,15 @@ class LLMSpec(BaseModel):
content = self.body.replace("<<PROMPT>>", escape_special_chars_for_json(prompt))
content = content.replace("<<BASE64_IMAGE>>", encoded_image)
content = content.replace("<<BASE64_AUDIO>>", encoded_audio)
async with httpx.AsyncClient() as client:
transport = httpx.AsyncHTTPTransport(retries=settings_var("network.retry", 3))
async with httpx.AsyncClient(transport=transport) as client:
response = await client.request(
method=self.method,
url=self.url,
headers=self.headers,
content=content,
timeout=(30, 90),
timeout=self.timeout(),
)
return response
@@ -169,6 +180,8 @@ def parse_http_spec(http_spec: str) -> LLMSpec:
has_audio = "<<BASE64_AUDIO>>" in body
for key, value in secrets.items():
if not value:
continue
key = key.strip("$")
body = body.replace(f"${key}", value)
+3 -3
View File
@@ -9,8 +9,8 @@ from rich.console import Console
from rich.table import Table
from tabulate import tabulate
from agentic_security.config import CfgMixin # Importing the configuration mixin
from agentic_security.models.schemas import Scan
from agentic_security.config import SettingsMixin # Importing the configuration mixin
from agentic_security.primitives import Scan
from agentic_security.probe_data import REGISTRY
from agentic_security.routes.scan import streaming_response_generator
@@ -23,7 +23,7 @@ YELLOW = colorama.Fore.YELLOW
BLUE = colorama.Fore.BLUE
class AgenticSecurity(CfgMixin):
class AgenticSecurity(SettingsMixin):
@classmethod
async def async_scan(
cls,
View File
+92
View File
@@ -0,0 +1,92 @@
from pyfiglet import Figlet, FontNotFound
from termcolor import colored
try:
from importlib.metadata import version
except ImportError:
from importlib_metadata import version
def generate_banner(
title="Agentic Security",
font="slant",
version="v2.1.0",
tagline="Proactive Threat Detection & Automated Security Protocols",
author="Developed by: [Security Team]",
website="Website: https://github.com/msoedov/agentic_security",
warning="",
):
"""Generate a visually enhanced banner with dynamic width and borders."""
# Define the text elements
# Initialize Figlet with the specified font, fallback to default if not found
try:
f = Figlet(font=font)
except FontNotFound:
f = Figlet() # Fallback to default font
# Render the title text and calculate the maximum width of Figlet lines
banner_text = f.renderText(title)
banner_lines = banner_text.splitlines()
figlet_max_width = max(len(line) for line in banner_lines) if banner_lines else 0
# Create the details line and calculate its width
details_line = f"Version: {version} | {website}"
details_width = len(details_line)
# Calculate widths of other text elements
warning_width = len(warning)
tagline_width = len(tagline)
# Determine the overall maximum width for centering
overall_max_width = max(
figlet_max_width, warning_width, tagline_width, details_width
)
# Pad the Figlet lines to the overall maximum width
padded_banner_lines = [line.center(overall_max_width) for line in banner_lines]
# Define decorative characters and colors
decor_chars = ["", "", ""]
decor_colors = ["blue", "red", "yellow"]
# Create and color the content lines
content_lines = []
for line in padded_banner_lines:
content_lines.append(colored(line, "blue"))
content_lines.append(colored(decor_chars[0] * overall_max_width, decor_colors[0]))
content_lines.append(
colored(warning.center(overall_max_width), "red", attrs=["blink", "bold"])
)
content_lines.append(colored(decor_chars[1] * overall_max_width, decor_colors[1]))
content_lines.append(colored(tagline.center(overall_max_width), "red"))
content_lines.append(colored(decor_chars[2] * overall_max_width, decor_colors[2]))
content_lines.append(colored(details_line.center(overall_max_width), "magenta"))
# Define border color and create top and bottom borders
border_color = "blue"
top_border = colored("" + "" * (overall_max_width + 2) + "", border_color)
bottom_border = colored("" + "" * (overall_max_width + 2) + "", border_color)
# Add side borders to each content line with padding
bordered_content = [
colored("", border_color) + line + colored("", border_color)
for line in content_lines
]
# Assemble the full banner
banner = top_border + "\n" + "\n".join(bordered_content) + "\n" + bottom_border
return banner
def init_banner():
ver = version("agentic_security")
try:
print(generate_banner(version=ver))
except Exception:
# UnicodeEncodeError with codec on some systems
pass
if __name__ == "__main__":
init_banner()
+11
View File
@@ -0,0 +1,11 @@
from agentic_security.primitives.models import ( # noqa
CompletionRequest,
FileProbeResponse,
LLMInfo,
Message,
Probe,
Scan,
ScanResult,
Settings,
Table,
)
+250 -218
View File
@@ -2,6 +2,7 @@ import asyncio
import random
import time
from collections.abc import AsyncGenerator
from json import JSONDecodeError
import httpx
import pandas as pd
@@ -10,7 +11,7 @@ from skopt import Optimizer
from skopt.space import Real
from agentic_security.http_spec import Modality
from agentic_security.models.schemas import Scan, ScanResult
from agentic_security.primitives import Scan, ScanResult
from agentic_security.probe_actor.cost_module import calculate_cost
from agentic_security.probe_actor.refusal import refusal_heuristic
from agentic_security.probe_data import audio_generator, image_generator, msj_data
@@ -19,6 +20,10 @@ from agentic_security.probe_data.data import prepare_prompts
# TODO: full log file
MAX_PROMPT_LENGTH = 2048
BUDGET_MULTIPLIER = 100_000_000
INITIAL_OPTIMIZER_POINTS = 25
MIN_FAILURE_SAMPLES = 5
FAILURE_RATE_THRESHOLD = 0.5
async def generate_prompts(
@@ -75,6 +80,42 @@ async def process_prompt(
logger.error(f"Request error: {exc}")
errors.append((module_name, prompt, "?", str(exc)))
return tokens, True
except JSONDecodeError as json_decode_error:
logger.error(f"Jason error: {json_decode_error}")
errors.append((module_name, prompt, "?", str(json_decode_error)))
return tokens, True
async def process_prompt_batch(
request_factory,
prompts: list[str],
tokens: int,
module_name: str,
refusals,
errors,
outputs,
) -> tuple[int, int]:
tasks = [
process_prompt(
request_factory, p, tokens, module_name, refusals, errors, outputs
)
for p in prompts
]
results = await asyncio.gather(*tasks)
total_tokens = sum(r[0] for r in results)
failures = sum(1 for r in results if r[1])
return total_tokens, failures
async def with_error_handling(agen):
try:
async for t in agen:
yield t
except Exception as e:
logger.exception("Scan failed")
yield ScanResult.status_msg(f"Scan failed: {str(e)}")
finally:
yield ScanResult.status_msg("Scan completed.")
async def perform_single_shot_scan(
@@ -87,126 +128,120 @@ async def perform_single_shot_scan(
secrets: dict[str, str] = {},
) -> AsyncGenerator[str, None]:
"""Perform a standard security scan."""
max_budget = max_budget * 100_000_000
max_budget = max_budget * BUDGET_MULTIPLIER
selected_datasets = [m for m in datasets if m["selected"]]
request_factory = multi_modality_spec(request_factory)
try:
yield ScanResult.status_msg("Loading datasets...")
prompt_modules = prepare_prompts(
dataset_names=[m["dataset_name"] for m in selected_datasets],
budget=max_budget,
tools_inbox=tools_inbox,
options=[m.get("opts", {}) for m in selected_datasets],
)
yield ScanResult.status_msg("Datasets loaded. Starting scan...")
yield ScanResult.status_msg("Loading datasets...")
prompt_modules = prepare_prompts(
dataset_names=[m["dataset_name"] for m in selected_datasets],
budget=max_budget,
tools_inbox=tools_inbox,
options=[m.get("opts", {}) for m in selected_datasets],
)
yield ScanResult.status_msg("Datasets loaded. Starting scan...")
errors = []
refusals = []
outputs = []
total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy)
processed_prompts = 0
errors = []
refusals = []
outputs = []
total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy)
processed_prompts = 0
optimizer = (
Optimizer([Real(0, 1)], base_estimator="GP", n_initial_points=25)
if optimize
else None
)
failure_rates = []
optimizer = (
Optimizer([Real(0, 1)], base_estimator="GP", n_initial_points=25)
if optimize
else None
)
failure_rates = []
total_tokens = 0
total_tokens = 0
tokens = 0
should_stop = False
for module in prompt_modules:
if should_stop:
break
tokens = 0
should_stop = False
for module in prompt_modules:
if should_stop:
break
tokens = 0
module_failures = 0
module_size = 0 if module.lazy else len(module.prompts)
logger.info(f"Scanning {module.dataset_name} {module_size}")
module_failures = 0
module_size = 0 if module.lazy else len(module.prompts)
logger.info(f"Scanning {module.dataset_name} {module_size}")
module_prompts = 0 # Reset for each module
async for prompt in generate_prompts(module.prompts):
if stop_event and stop_event.is_set():
stop_event.clear()
logger.info("Scan stopped by user.")
yield ScanResult.status_msg("Scan stopped by user.")
return
async for prompt in generate_prompts(module.prompts):
if stop_event and stop_event.is_set():
stop_event.clear()
logger.info("Scan stopped by user.")
yield ScanResult.status_msg("Scan stopped by user.")
return
processed_prompts += 1
progress = (
100 * processed_prompts / total_prompts if total_prompts else 0
)
total_tokens -= tokens
start = time.time()
tokens, failed = await process_prompt(
request_factory,
prompt,
tokens,
module.dataset_name,
refusals,
errors,
outputs,
)
end = time.time()
total_tokens += tokens
# logger.debug(f"Trying prompt: {prompt}, {failed=}")
if failed:
module_failures += 1
failure_rate = module_failures / max(processed_prompts, 1)
failure_rates.append(failure_rate)
cost = calculate_cost(tokens)
processed_prompts += 1
module_prompts += 1 # Fixed increment syntax
# Calculate progress based on total processed prompts
progress = 100 * processed_prompts / total_prompts if total_prompts else 0
progress = progress % 100
# TODO: improve this cond
last_output = outputs[-1] if outputs else None
if last_output and last_output[1] == prompt:
response_text = last_output[2]
else:
response_text = ""
total_tokens -= tokens
start = time.time()
tokens, failed = await process_prompt(
request_factory,
prompt,
tokens,
module.dataset_name,
refusals,
errors,
outputs,
)
end = time.time()
total_tokens += tokens
yield ScanResult(
module=module.dataset_name,
tokens=round(tokens / 1000, 1),
cost=cost,
progress=round(progress, 2),
failureRate=round(failure_rate * 100, 2),
prompt=prompt[:MAX_PROMPT_LENGTH],
latency=end - start,
model=response_text,
).model_dump_json()
if failed:
module_failures += 1
failure_rate = module_failures / max(module_prompts, 1)
failure_rates.append(failure_rate)
cost = calculate_cost(tokens)
if optimize and len(failure_rates) >= 5:
next_point = optimizer.ask()
optimizer.tell(next_point, -failure_rate)
best_failure_rate = -optimizer.get_result().fun
if best_failure_rate > 0.5:
yield ScanResult.status_msg(
f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..."
)
should_stop = True
break
if total_tokens > max_budget:
logger.info(
f"Scan ran out of budget and stopped. {total_tokens=} {max_budget=}"
)
last_output = outputs[-1] if outputs else None
if last_output and last_output[1] == prompt:
response_text = last_output[2]
else:
response_text = ""
yield ScanResult(
module=module.dataset_name,
tokens=round(tokens / 1000, 1),
cost=cost,
progress=round(progress, 2),
failureRate=round(failure_rate * 100, 2),
prompt=prompt[:MAX_PROMPT_LENGTH],
latency=end - start,
model=response_text,
).model_dump_json()
if optimize and len(failure_rates) >= 5:
next_point = optimizer.ask()
optimizer.tell(next_point, -failure_rate)
best_failure_rate = -optimizer.get_result().fun
if best_failure_rate > 0.5:
yield ScanResult.status_msg(
f"Scan ran out of budget and stopped. {total_tokens=} {max_budget=}"
f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..."
)
should_stop = True
break
if total_tokens > max_budget:
logger.info(
f"Scan ran out of budget and stopped. {total_tokens=} {max_budget=}"
)
yield ScanResult.status_msg(
f"Scan ran out of budget and stopped. {total_tokens=} {max_budget=}"
)
should_stop = True
break
yield ScanResult.status_msg("Scan completed.")
yield ScanResult.status_msg("Scan completed.")
failure_data = errors + refusals
df = pd.DataFrame(
failure_data, columns=["module", "prompt", "status_code", "content"]
)
df.to_csv("failures.csv", index=False)
except Exception as e:
logger.exception("Scan failed")
yield ScanResult.status_msg(f"Scan failed: {str(e)}")
# raise e
finally:
yield ScanResult.status_msg("Scan completed.")
failure_data = errors + refusals
df = pd.DataFrame(
failure_data, columns=["module", "prompt", "status_code", "content"]
)
df.to_csv("failures.csv", index=False)
async def perform_many_shot_scan(
@@ -223,114 +258,107 @@ async def perform_many_shot_scan(
) -> AsyncGenerator[str, None]:
"""Perform a multi-step security scan with probe injection."""
request_factory = multi_modality_spec(request_factory)
try:
# Load main and probe datasets
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,
tools_inbox=tools_inbox,
)
yield ScanResult.status_msg("Loading datasets for MSJ...")
msj_modules = msj_data.prepare_prompts(probe_datasets)
yield ScanResult.status_msg("Datasets loaded. Starting scan...")
# Load main and probe datasets
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,
tools_inbox=tools_inbox,
)
yield ScanResult.status_msg("Loading datasets for MSJ...")
msj_modules = msj_data.prepare_prompts(probe_datasets)
yield ScanResult.status_msg("Datasets loaded. Starting scan...")
errors = []
refusals = []
outputs = []
total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy)
processed_prompts = 0
errors = []
refusals = []
outputs = []
total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy)
processed_prompts = 0
optimizer = (
Optimizer([Real(0, 1)], base_estimator="GP", n_initial_points=25)
if optimize
else None
)
failure_rates = []
optimizer = (
Optimizer([Real(0, 1)], base_estimator="GP", n_initial_points=25)
if optimize
else None
)
failure_rates = []
for module in prompt_modules:
module_failures = 0
module_size = 0 if module.lazy else len(module.prompts)
logger.info(f"Scanning {module.dataset_name} {module_size}")
for module in prompt_modules:
module_failures = 0
module_size = 0 if module.lazy else len(module.prompts)
logger.info(f"Scanning {module.dataset_name} {module_size}")
async for prompt in generate_prompts(module.prompts):
if stop_event and stop_event.is_set():
stop_event.clear()
logger.info("Scan stopped by user.")
yield ScanResult.status_msg("Scan stopped by user.")
return
tokens = 0
processed_prompts += 1
progress = (
100 * processed_prompts / total_prompts if total_prompts else 0
async for prompt in generate_prompts(module.prompts):
if stop_event and stop_event.is_set():
stop_event.clear()
logger.info("Scan stopped by user.")
yield ScanResult.status_msg("Scan stopped by user.")
return
tokens = 0
processed_prompts += 1
progress = 100 * processed_prompts / total_prompts if total_prompts else 0
progress = progress % 100
full_prompt = ""
msj_module = random.choice(msj_modules)
prompt_tokens = len(full_prompt.split())
tokens += prompt_tokens
injected = False
for _ in range(20):
if injected:
break
m_prompt = random.choice(msj_module.prompts)
full_prompt += "\n" + m_prompt
if tokens > max_ctx_length:
full_prompt = "\n" + prompt
injected = True
tokens, failed = await process_prompt(
request_factory,
full_prompt,
tokens,
module.dataset_name,
refusals,
errors,
outputs,
)
if failed:
module_failures += 1
break
if injected:
break
full_prompt = ""
msj_module = random.choice(msj_modules)
failure_rate = module_failures / max(processed_prompts, 1)
failure_rates.append(failure_rate)
cost = calculate_cost(tokens)
prompt_tokens = len(full_prompt.split())
tokens += prompt_tokens
yield ScanResult(
module=module.dataset_name,
tokens=round(tokens / 1000, 1),
cost=cost,
progress=round(progress, 2),
failureRate=round(failure_rate * 100, 2),
prompt=prompt[:MAX_PROMPT_LENGTH],
).model_dump_json()
injected = False
for _ in range(20):
if injected:
break
m_prompt = random.choice(msj_module.prompts)
full_prompt += "\n" + m_prompt
if tokens > max_ctx_length:
full_prompt = "\n" + prompt
injected = True
tokens, failed = await process_prompt(
request_factory,
full_prompt,
tokens,
module.dataset_name,
refusals,
errors,
outputs,
if optimize and len(failure_rates) >= 5:
next_point = optimizer.ask()
optimizer.tell(next_point, -failure_rate)
best_failure_rate = -optimizer.get_result().fun
if best_failure_rate > 0.5:
yield ScanResult.status_msg(
f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..."
)
if failed:
module_failures += 1
break
if injected:
break
break
failure_rate = module_failures / max(processed_prompts, 1)
failure_rates.append(failure_rate)
cost = calculate_cost(tokens)
yield ScanResult.status_msg("Scan completed.")
yield ScanResult(
module=module.dataset_name,
tokens=round(tokens / 1000, 1),
cost=cost,
progress=round(progress, 2),
failureRate=round(failure_rate * 100, 2),
prompt=prompt[:MAX_PROMPT_LENGTH],
).model_dump_json()
if optimize and len(failure_rates) >= 5:
next_point = optimizer.ask()
optimizer.tell(next_point, -failure_rate)
best_failure_rate = -optimizer.get_result().fun
if best_failure_rate > 0.5:
yield ScanResult.status_msg(
f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..."
)
break
yield ScanResult.status_msg("Scan completed.")
df = pd.DataFrame(
errors + refusals, columns=["module", "prompt", "status_code", "content"]
)
df.to_csv("failures.csv", index=False)
except Exception as e:
logger.exception("Scan failed")
yield ScanResult.status_msg(f"Scan failed: {str(e)}")
raise e
df = pd.DataFrame(
errors + refusals, columns=["module", "prompt", "status_code", "content"]
)
df.to_csv("failures.csv", index=False)
def scan_router(
@@ -340,23 +368,27 @@ def scan_router(
stop_event: asyncio.Event = None,
):
if scan_parameters.enableMultiStepAttack:
return perform_many_shot_scan(
request_factory=request_factory,
max_budget=scan_parameters.maxBudget,
datasets=scan_parameters.datasets,
probe_datasets=scan_parameters.probe_datasets,
tools_inbox=tools_inbox,
optimize=scan_parameters.optimize,
stop_event=stop_event,
secrets=scan_parameters.secrets,
return with_error_handling(
perform_many_shot_scan(
request_factory=request_factory,
max_budget=scan_parameters.maxBudget,
datasets=scan_parameters.datasets,
probe_datasets=scan_parameters.probe_datasets,
tools_inbox=tools_inbox,
optimize=scan_parameters.optimize,
stop_event=stop_event,
secrets=scan_parameters.secrets,
)
)
else:
return perform_single_shot_scan(
request_factory=request_factory,
max_budget=scan_parameters.maxBudget,
datasets=scan_parameters.datasets,
tools_inbox=tools_inbox,
optimize=scan_parameters.optimize,
stop_event=stop_event,
secrets=scan_parameters.secrets,
return with_error_handling(
perform_single_shot_scan(
request_factory=request_factory,
max_budget=scan_parameters.maxBudget,
datasets=scan_parameters.datasets,
tools_inbox=tools_inbox,
optimize=scan_parameters.optimize,
stop_event=stop_event,
secrets=scan_parameters.secrets,
)
)
+83 -107
View File
@@ -3,10 +3,13 @@ import logging
from typing import Any
import httpx
from httpx import LLMSpec
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
from agentic_security.http_spec import LLMSpec
LLM_SPECS = []
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@@ -28,6 +31,7 @@ class OperatorToolBox:
self.spec = spec
self.datasets = datasets
self.failures = []
self.llm_specs = [LLMSpec.from_string(spec) for spec in LLM_SPECS]
def get_spec(self) -> AgentSpecification:
return self.spec
@@ -62,52 +66,33 @@ class OperatorToolBox:
return f"Operation '{operation}' failed: Dataset not found."
return f"Operation '{operation}' executed successfully."
async def test(self, description: str, sample_test: dict[str, Any]) -> str:
agent = Agent(
"openai:gpt-4o",
result_type=LLMSpec,
system_prompt="Extract the LLM specification from the input",
)
async def test_llm_spec(self, llm_spec: LLMSpec, user_prompt: str) -> str:
try:
# Verify the spec
response = await llm_spec.verify()
response.raise_for_status()
logger.info(f"Verification succeeded for {llm_spec.url}")
async with agent.run_stream(description) as result:
async for spec in result.stream():
self.spec.endpoint = spec.url
# Run test with user prompt
test_response = await llm_spec.probe(user_prompt)
test_response.raise_for_status()
response_data = test_response.json()
return f"Test succeeded for {llm_spec.url}: {response_data}"
except httpx.HTTPStatusError as e:
self.failures.append(f"HTTP error occurred: {e}")
logger.error(f"Test failed for {llm_spec.url}: {e}")
return f"Test failed for {llm_spec.url}: {e}"
except Exception as e:
self.failures.append(f"An error occurred: {e}")
logger.error(f"Test failed for {llm_spec.url}: {e}")
return f"Test failed for {llm_spec.url}: {e}"
# Verify access to the endpoint
async with httpx.AsyncClient() as client:
try:
access_response = await client.get(spec.url)
access_response.raise_for_status()
except httpx.HTTPStatusError as e:
self.failures.append(f"HTTP error occurred: {e}")
logger.error(f"Access verification failed: {e}")
return f"Access verification failed: {e}"
except Exception as e:
self.failures.append(f"An error occurred: {e}")
logger.error(f"Access verification failed: {e}")
return f"Access verification failed: {e}"
async def test_with_prompt(self, spec_index: int, user_prompt: str) -> str:
if not 0 <= spec_index < len(self.llm_specs):
return f"Invalid spec index: {spec_index}. Valid range is 0 to {len(self.llm_specs) - 1}"
# Run the sample test
try:
test_response = await client.post(
f"{spec.url}/test", json=sample_test
)
test_response.raise_for_status()
response_data = test_response.json()
if "choices" in response_data and len(response_data["choices"]) > 0:
return f"Testing agent at {spec.url} succeeded: {response_data}"
else:
self.failures.append("Invalid response format")
logger.error("Sample test failed: Invalid response format")
return "Sample test failed: Invalid response format"
except httpx.HTTPStatusError as e:
self.failures.append(f"HTTP error occurred: {e}")
logger.error(f"Sample test failed: {e}")
return f"Sample test failed: {e}"
except Exception as e:
self.failures.append(f"An error occurred: {e}")
logger.error(f"Sample test failed: {e}")
return f"Sample test failed: {e}"
llm_spec = self.llm_specs[spec_index]
return await self.test_llm_spec(llm_spec, user_prompt)
# Initialize OperatorToolBox with AgentSpecification
@@ -126,104 +111,95 @@ dataset_manager_agent = Agent(
model="gpt-4",
deps_type=OperatorToolBox,
result_type=str,
system_prompt="You can validate the toolbox, run operations, and retrieve results or failures.",
system_prompt="You can validate the toolbox, run operations, retrieve results or failures, and test LLM specs.",
)
@dataset_manager_agent.tool
async def validate_toolbox(ctx: RunContext[OperatorToolBox]) -> str:
is_valid = ctx.deps.validate()
if is_valid:
return "ToolBox validation successful."
else:
return "ToolBox validation failed."
return (
"ToolBox validation successful." if is_valid else "ToolBox validation failed."
)
@dataset_manager_agent.tool
async def execute_operation(ctx: RunContext[OperatorToolBox], operation: str) -> str:
result = ctx.deps.run_operation(operation)
return result
return ctx.deps.run_operation(operation)
@dataset_manager_agent.tool
async def retrieve_results(ctx: RunContext[OperatorToolBox]) -> str:
results = ctx.deps.get_results()
if results:
formatted_results = "\n".join([f"{op}: {res}" for op, res in results.items()])
return f"Operation Results:\n{formatted_results}"
else:
return "No operations have been executed yet."
return (
f"Operation Results:\n{results}"
if results
else "No operations have been executed yet."
)
@dataset_manager_agent.tool
async def retrieve_failures(ctx: RunContext[OperatorToolBox]) -> str:
failures = ctx.deps.get_failures()
if failures:
formatted_failures = "\n".join(failures)
return f"Failures:\n{formatted_failures}"
else:
return "No failures recorded."
return f"Failures:\n{failures}" if failures else "No failures recorded."
@dataset_manager_agent.tool
async def test_agent(
ctx: RunContext[OperatorToolBox], description: str, sample_test: dict[str, Any]
async def list_llm_specs(ctx: RunContext[OperatorToolBox]) -> str:
spec_list = "\n".join(
f"{i}: {spec.url}" for i, spec in enumerate(ctx.deps.llm_specs)
)
return f"Available LLM Specs:\n{spec_list}"
@dataset_manager_agent.tool
async def test_llm_with_prompt(
ctx: RunContext[OperatorToolBox], spec_index: int, user_prompt: str
) -> str:
result = await ctx.deps.test(description, sample_test)
return result
return await ctx.deps.test_with_prompt(spec_index, user_prompt)
# Synchronous run example
def run_dataset_manager_agent_sync():
prompts = [
"Validate the toolbox.",
"Execute operation on 'dataset2'.",
"Execute operation on 'dataset4'.", # This should fail
"Retrieve the results.",
"Retrieve any failures.",
"Test my openAI compatible agent deployed at localhost:3000",
]
sample_test = {"prompt": "Hello, how are you?", "max_tokens": 5}
for prompt in prompts:
if "Test my" in prompt:
result = dataset_manager_agent.run_sync(
prompt, deps=toolbox, sample_test=sample_test
)
else:
result = dataset_manager_agent.run_sync(prompt, deps=toolbox)
print(f"Prompt: {prompt}")
print(f"Response: {result.data}\n")
# Asynchronous run example
# Asynchronous run example with user confirmation
async def run_dataset_manager_agent_async():
prompts = [
"Validate the toolbox.",
"Execute operation on 'dataset2'.",
"Execute operation on 'dataset4'.", # This should fail
"Retrieve the results.",
"Retrieve any failures.",
"Test my openAI compatible agent deployed at localhost:3000",
"List available LLM specs.",
"I want to test an LLM with my prompt: 'Tell me a short story about a robot'. Which spec index should I use?",
]
sample_test = {"prompt": "Hello, how are you?", "max_tokens": 5}
for prompt in prompts:
if "Test my" in prompt:
result = await dataset_manager_agent.run(
prompt, deps=toolbox, sample_test=sample_test
)
else:
result = await dataset_manager_agent.run(prompt, deps=toolbox)
result = await dataset_manager_agent.run(prompt, deps=toolbox)
print(f"Prompt: {prompt}")
print(f"Response: {result.data}\n")
# Handle testing request
if "test an LLM with my prompt" in prompt:
print(
"Please select a spec index from the list above and confirm to proceed."
)
# Simulate user input for demo (in real app, you'd get this from user)
user_input = (
input("Enter spec index and 'yes' to confirm (e.g., '0 yes'): ")
.strip()
.split()
)
if len(user_input) == 2 and user_input[1].lower() == "yes":
try:
spec_index = int(user_input[0])
# Extract prompt from the original input
user_prompt = prompt.split("my prompt: ")[1].strip("'")
test_result = await dataset_manager_agent.run(
f"Test LLM at index {spec_index} with prompt: {user_prompt}",
deps=toolbox,
spec_index=spec_index,
user_prompt=user_prompt,
)
print(f"Test Response: {test_result.data}\n")
except ValueError:
print("Invalid spec index provided.\n")
else:
print("Test canceled. Please provide a valid index and confirmation.\n")
if __name__ == "__main__":
# Run synchronous example
run_dataset_manager_agent_sync()
# Run asynchronous example
asyncio.run(run_dataset_manager_agent_async())
+1 -1
View File
@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod
from agentic_security.refusal_classifier import RefusalClassifier
from agentic_security.refusal_classifier.model import RefusalClassifier
classifier = RefusalClassifier()
classifier.load_model()
@@ -1,4 +1,5 @@
from agentic_security.probe_actor.refusal import DefaultRefusalClassifier
from agentic_security.probe_actor.refusal import refusal_heuristic, RefusalClassifierManager, RefusalClassifierPlugin
class TestCheckRefusal:
@@ -11,3 +12,40 @@ class TestCheckRefusal:
def test_empty_response(self):
response = ""
assert not DefaultRefusalClassifier().is_refusal(response)
def test_non_refusal_phrase(self):
"""Test that a response without refusal phrases returns False."""
response = "Here is some helpful information."
assert not DefaultRefusalClassifier().is_refusal(response)
def test_refusal_heuristic(self):
"""Test that the refusal_heuristic function correctly identifies refusal phrases."""
response = "Sorry, I cannot assist with your request."
# refusal_heuristic will cast the argument to a string and use the plugin manager to check for refusal markers
assert refusal_heuristic(response)
def test_plugin_manager_with_dummy_plugin(self):
"""Test that RefusalClassifierManager returns True if any registered plugin returns True."""
class AlwaysFalsePlugin(RefusalClassifierPlugin):
def is_refusal(self, response: str) -> bool:
return False
class AlwaysTruePlugin(RefusalClassifierPlugin):
def is_refusal(self, response: str) -> bool:
return True
manager = RefusalClassifierManager()
manager.register_plugin("false", AlwaysFalsePlugin())
manager.register_plugin("true", AlwaysTruePlugin())
response = "Any response text"
assert manager.is_refusal(response)
def test_default_classifier_with_empty_phrases(self):
"""Test that DefaultRefusalClassifier returns False when provided with an empty refusal phrase list."""
classifier = DefaultRefusalClassifier(refusal_phrases=[])
response = "I do not have any info."
assert not classifier.is_refusal(response)
def test_case_sensitivity(self):
"""Test that string matching is case-sensitive."""
response = "i'm sorry, but can you help me?" # lower-case "i'm sorry" does not match "I'm sorry" and no extra refusal phrases are present
assert not DefaultRefusalClassifier().is_refusal(response)
+39
View File
@@ -5,6 +5,7 @@ REGISTRY_V0 = [
"dataset_name": "simonycl/aya-23-8B_advbench_jailbreak",
"num_prompts": 416,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -16,6 +17,7 @@ REGISTRY_V0 = [
"dataset_name": "acmc/jailbreaks_dataset_with_perplexity_bigcode_starcoder2-3b_bigcode_starcoder2-7b",
"num_prompts": 11191,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -27,6 +29,7 @@ REGISTRY_V0 = [
"dataset_name": "karanxa/dolphin-jailbreak-finetuning-dataset",
"num_prompts": 42684,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -38,6 +41,7 @@ REGISTRY_V0 = [
"dataset_name": "karanxa/llama-2-jailbreak-dataset",
"num_prompts": 40613,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -49,6 +53,7 @@ REGISTRY_V0 = [
"dataset_name": "karanxa/llama2-uncensored-jailbreak-dataset-finetuning",
"num_prompts": 42854,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -60,6 +65,7 @@ REGISTRY_V0 = [
"dataset_name": "liuyanchen1015/Llama-3.2-1B_jailbreak_responses",
"num_prompts": 9888,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -71,6 +77,7 @@ REGISTRY_V0 = [
"dataset_name": "liuyanchen1015/Llama-3.2-1B-Instruct_jailbreak_responses",
"num_prompts": 9888,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -82,6 +89,7 @@ REGISTRY_V0 = [
"dataset_name": "liuyanchen1015/Llama-3.2-1B-Instruct_jailbreak_responses_with_judgment",
"num_prompts": 9888,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -93,6 +101,7 @@ REGISTRY_V0 = [
"dataset_name": "jackhhao/jailbreak-classification",
"num_prompts": 1044,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -104,6 +113,7 @@ REGISTRY_V0 = [
"dataset_name": "markush1/LLM-Jailbreak-Classifier",
"num_prompts": 201193,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -115,6 +125,7 @@ REGISTRY_V0 = [
"dataset_name": "walledai/JailbreakBench",
"num_prompts": 200,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -126,6 +137,7 @@ REGISTRY_V0 = [
"dataset_name": "walledai/JailbreakHub",
"num_prompts": 15140,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -137,6 +149,7 @@ REGISTRY_V0 = [
"dataset_name": "Granther/evil-jailbreak",
"num_prompts": 1200,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -148,6 +161,7 @@ REGISTRY_V0 = [
"dataset_name": "sevdeawesome/jailbreak_success",
"num_prompts": 10800,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -159,6 +173,7 @@ REGISTRY_V0 = [
"dataset_name": "IDA-SERICS/Disaster-tweet-jailbreaking",
"num_prompts": 3000,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -170,6 +185,7 @@ REGISTRY_V0 = [
"dataset_name": "GeorgeDaDude/Jailbreak_Complete_DS_labeled",
"num_prompts": 11383,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -181,6 +197,7 @@ REGISTRY_V0 = [
"dataset_name": "dayone3nder/jailbreak_prompt_JBB_sft_trainset",
"num_prompts": 4785,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -192,6 +209,7 @@ REGISTRY_V0 = [
"dataset_name": "dayone3nder/general_safe_mix_jailbreak_prompt_JBB_trainset",
"num_prompts": 24679,
"tokens": None, # Add actual token count if available
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -206,6 +224,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "AgenticBackend",
"num_prompts": 2000,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Fine-tuned cloud hosted model",
"selected": True,
@@ -221,6 +240,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "ShawnMenz/DAN_jailbreak",
"num_prompts": 666,
"tokens": 224196,
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -232,6 +252,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "deepset/prompt-injections",
"num_prompts": 203,
"tokens": 6988,
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -243,6 +264,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "rubend18/ChatGPT-Jailbreak-Prompts",
"num_prompts": 79,
"tokens": 26971,
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -254,6 +276,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "notrichardren/refuse-to-answer-prompts",
"num_prompts": 522,
"tokens": 7172,
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -265,6 +288,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "Lemhf14/EasyJailbreak_Datasets",
"num_prompts": 1630,
"tokens": 19758,
"is_active": False,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -276,6 +300,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "markush1/LLM-Jailbreak-Classifier",
"num_prompts": 1119,
"tokens": 19758,
"is_active": True,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -287,6 +312,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "JailbreakV-28K/JailBreakV-28k",
"num_prompts": 28300,
"tokens": 1975800,
"is_active": False,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -298,6 +324,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "ShawnMenz/jailbreak_sft_rm_ds",
"num_prompts": 371000,
"tokens": 1975800,
"is_active": False,
"approx_cost": 0.0,
"source": "Hugging Face Datasets",
"selected": False,
@@ -309,6 +336,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "Steganography",
"num_prompts": 10,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Local mutation dataset",
"selected": False,
@@ -320,6 +348,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "GPT fuzzer",
"num_prompts": 10,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Local mutation dataset",
"selected": False,
@@ -331,6 +360,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "jailbreak_llms/2023_05_07",
"num_prompts": 0,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Github",
"selected": False,
@@ -342,6 +372,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "jailbreak_llms/2023_12_25.csv",
"num_prompts": 0,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Github",
"selected": False,
@@ -353,6 +384,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "Malwaregen",
"num_prompts": 0,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Local dataset",
"selected": False,
@@ -364,6 +396,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "Hallucination",
"num_prompts": 0,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Local dataset",
"selected": False,
@@ -375,6 +408,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "DataLeak",
"num_prompts": 0,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Local dataset",
"selected": False,
@@ -386,6 +420,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "llm-adaptive-attacks",
"num_prompts": 0,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Github: tml-epfl/llm-adaptive-attacks#0.0.1",
"selected": False,
@@ -397,6 +432,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "Garak",
"num_prompts": 0,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Github: https://github.com/leondz/garak#v0.9.0.1",
"selected": False,
@@ -412,6 +448,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "Reinforcement Learning Optimization",
"num_prompts": 0,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Cloud hosted model",
"selected": False,
@@ -427,6 +464,7 @@ REGISTRY = REGISTRY_V0 + [
"dataset_name": "InspectAI",
"num_prompts": 0,
"tokens": 0,
"is_active": True,
"approx_cost": 0.0,
"source": "Github: https://github.com/UKGovernmentBEIS/inspect_ai",
"selected": False,
@@ -439,6 +477,7 @@ REGISTRY = REGISTRY_V0 + [
"num_prompts": len(load_local_csv().prompts),
"tokens": load_local_csv().tokens,
"approx_cost": 0.0,
"is_active": True,
"source": f"Local file dataset: {load_local_csv().metadata['src']}",
"selected": len(load_local_csv().prompts),
"url": "",
+9 -1
View File
@@ -3,7 +3,7 @@ import random
from fastapi import APIRouter, File, Header, HTTPException, UploadFile
from fastapi.responses import JSONResponse
from ..models.schemas import FileProbeResponse, Probe
from ..primitives import FileProbeResponse, Probe
from ..probe_actor.refusal import REFUSAL_MARKS
from ..probe_data import REGISTRY
@@ -77,3 +77,11 @@ async def data_config():
async def health_check():
"""Health check endpoint."""
return JSONResponse(content={"status": "ok"})
@router.post("/v1/self-probe-t5")
def self_probe_t5(probe: Probe):
import languagemodels as lm # noqa
message = lm.do(probe.prompt)
return make_mock_response(message)
+1 -1
View File
@@ -5,7 +5,7 @@ from fastapi import APIRouter
from loguru import logger
from ..core.app import get_current_run, get_tools_inbox
from ..models.schemas import CompletionRequest, Settings
from ..primitives import CompletionRequest, Settings
from ..probe_actor.refusal import REFUSAL_MARKS
router = APIRouter()
+1 -1
View File
@@ -3,7 +3,7 @@ from pathlib import Path
from fastapi import APIRouter, Response
from fastapi.responses import FileResponse, StreamingResponse
from ..models.schemas import Table
from ..primitives import Table
from ..report_chart import plot_security_report
router = APIRouter()
+8 -2
View File
@@ -10,11 +10,12 @@ from fastapi import (
UploadFile,
)
from fastapi.responses import StreamingResponse
from loguru import logger
from ..core.app import get_stop_event, get_tools_inbox, set_current_run
from ..dependencies import InMemorySecrets, get_in_memory_secrets
from ..http_spec import LLMSpec
from ..models.schemas import LLMInfo, Scan
from ..primitives import LLMInfo, Scan
from ..probe_actor import fuzzer
router = APIRouter()
@@ -25,7 +26,12 @@ async def verify(
info: LLMInfo, secrets: InMemorySecrets = Depends(get_in_memory_secrets)
):
spec = LLMSpec.from_string(info.spec)
r = await spec.verify()
try:
r = await spec.verify()
except Exception as e:
logger.exception(e)
raise HTTPException(status_code=400, detail=str(e))
if r.status_code >= 400:
raise HTTPException(status_code=r.status_code, detail=r.text)
return dict(
+1 -1
View File
@@ -7,7 +7,7 @@ from fastapi.templating import Jinja2Templates
from jinja2 import Environment, FileSystemLoader
from starlette.responses import Response
from ..models.schemas import Settings
from ..primitives import Settings
router = APIRouter()
STATIC_DIR = Path(__file__).parent.parent / "static"
+2 -1
View File
@@ -2,7 +2,7 @@ import sentry_sdk
from loguru import logger
from sentry_sdk.integrations.logging import ignore_logger
from ..models.schemas import Settings
from ..primitives import Settings
def setup(app):
@@ -16,6 +16,7 @@ def setup(app):
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for tracing.
traces_sample_rate=1.0,
ignore_errors=[KeyboardInterrupt],
_experiments={
# Set continuous_profiling_auto_start to True
# to automatically start the profiler on when
+17 -1
View File
@@ -1,4 +1,3 @@
let SELF_URL = window.location.href;
if (SELF_URL.endsWith('/')) {
SELF_URL = SELF_URL.slice(0, -1);
@@ -171,6 +170,21 @@ Content-Type: application/json
{
"audio_url": "<<AUDIO_FILE_URL>>"
}
`,
`POST https://api.openrouter.ai/v1/chat/completions
Authorization: Bearer $OPENROUTER_API_KEY
Content-Type: application/json
{
"model": "openrouter-latest",
"prompt": "<<PROMPT>>",
"temperature": 0.7,
"max_tokens": 150,
"top_p": 0.9,
"frequency_penalty": 0,
"presence_penalty": 0
}
`,
]
@@ -190,6 +204,7 @@ let LLM_CONFIGS = [
{ name: 'Claude', prompts: 40000, logo: '/icons/claude.png' },
{ name: 'Cohere', prompts: 40000, logo: '/icons/cohere.png' },
{ name: 'Azure OpenAI', prompts: 40000, logo: '/icons/azureai.png' },
{ name: 'OpenRouter.ai', prompts: 40000, logo: '/icons/openrouter.png' },
{ name: 'assemblyai', prompts: 40000, logo: fallbackIcon },
];
function has_image(spec) {
@@ -226,5 +241,6 @@ function _getFailureRateScore(failureRate) {
else if (strengthRate >= 80) return 'B';
else if (strengthRate >= 70) return 'C';
else if (strengthRate >= 60) return 'D';
else if (strengthRate >= 1) return '?';
else return 'E'; // For strengthRate less than 60
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+19 -20
View File
@@ -383,27 +383,26 @@
class="text-gray-400 hover:underline">Deselect All</button>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div
v-for="(package, index) in dataConfig"
:key="index"
@click="addPackage(index)"
class="border rounded-lg p-3 cursor-pointer transition-all hover:shadow-md overflow-hidden"
:class="{
'border-dark-accent-green bg-dark-accent-green bg-opacity-20': package.selected,
'border-gray-600': !package.selected
}">
<div class="font-medium mb-1 truncate">{{ package.dataset_name
}}</div>
<div class="text-sm text-gray-400 truncate">
{{ package.source || 'Local dataset' }}
</div>
<div class="mt-2 text-sm font-semibold">
{{ package.dynamic ? 'Dynamic dataset' :
`${package.num_prompts.toLocaleString()} prompts` }}
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div
v-for="(package, index) in dataConfig"
:key="index"
@click="package.is_active !== false && addPackage(index)"
class="border rounded-lg p-3 cursor-pointer transition-all hover:shadow-md overflow-hidden"
:class="{
'border-dark-accent-green bg-dark-accent-green bg-opacity-20': package.selected,
'border-gray-600': !package.selected,
'opacity-30 pointer-events-none cursor-not-allowed': package.is_active === false
}">
<div class="font-medium mb-1 truncate">{{ package.dataset_name }}</div>
<div class="text-sm text-gray-400 truncate">
{{ package.source || 'Local dataset' }}
</div>
<div class="mt-2 text-sm font-semibold">
{{ package.dynamic ? 'Dynamic dataset' : `${package.num_prompts.toLocaleString()} prompts` }}
</div>
</div>
</div>
</div>
</section>
+4
View File
@@ -350,6 +350,10 @@ var app = new Vue({
// If all are selected, deselect all. Otherwise, select all.
this.dataConfig.forEach(package => {
if (!package.is_active) {
package.selected = false;
return
}
package.selected = !allSelected;
});
+7 -6
View File
@@ -50,7 +50,7 @@ def make_test_registry():
]
class TestAS:
class TestLibraryLevel:
# Handles an empty dataset list.
def test_class(self, test_server):
llmSpec = test_spec_assets.SAMPLE_SPEC
@@ -62,8 +62,8 @@ class TestAS:
print(result)
assert len(result) in [0, 1]
# TODO: slow test
def _test_class_msj(self, test_server):
@pytest.mark.slow
def test_class_msj(self, test_server):
llmSpec = test_spec_assets.SAMPLE_SPEC
maxBudget = 1000
max_th = 0.3
@@ -98,6 +98,7 @@ class TestAS:
print(result)
assert len(result) in [0, 1]
@pytest.mark.slow
def test_backend(self, test_server):
llmSpec = test_spec_assets.SAMPLE_SPEC
maxBudget = 1000000
@@ -156,7 +157,7 @@ class TestAS:
class TestEntrypointCI:
def test_generate_default_cfg_to_tmp_path(self):
"""
Test that the `generate_default_cfg` method generates a valid default config file in a temporary path.
Test that the `generate_default_settings` method generates a valid default config file in a temporary path.
"""
# Create a temporary directory
with tempfile.TemporaryDirectory() as tmpdir:
@@ -167,7 +168,7 @@ class TestEntrypointCI:
# Generate the default configuration
security = AgenticSecurity()
security.generate_default_cfg()
security.generate_default_settings()
# Check that the config file was created at the temporary path
assert os.path.exists(temp_path), f"{temp_path} file should be generated."
@@ -192,7 +193,7 @@ class TestEntrypointCI:
# Generate the default configuration
security = AgenticSecurity()
security.generate_default_cfg()
security.generate_default_settings()
# Load the generated configuration
AgenticSecurity.load_config(temp_path)
+2
View File
@@ -0,0 +1,2 @@
from: python-pytest-poetry
# This file was generated automatically by CodeBeaver based on your repository. Learn how to customize it here: https://docs.codebeaver.ai/configuration/
+1 -1
View File
@@ -21,4 +21,4 @@ Note: Please be aware that Agentic Security is designed as a safety scanner tool
## UI 🧙
<img width="100%" alt="booking-screen" src="https://res.cloudinary.com/dq0w2rtm9/image/upload/v1736433557/z0bsyzhsqlgcr3w4ovwp.gif">
<img width="100%" alt="booking-screen" src="https://res.cloudinary.com/dq0w2rtm9/image/upload/v1741192668/final_aa9jhb.gif">
+13 -9
View File
@@ -1,14 +1,16 @@
:root {
--md-primary-fg-color: #e92063;
--md-primary-fg-color--light: #e92063;
--md-primary-fg-color--dark: #e92063;
--md-primary-fg-color: #2E4053;
/* Primary color changed to pinkish */
--md-primary-fg-color--light: #E0A3B6;
--md-primary-fg-color--dark: #1C3F74;
/* Dark variant changed to blue */
}
/* Revert hue value to that of pre mkdocs-material v9.4.0 */
/* Updated slate color scheme with new background */
[data-md-color-scheme="slate"] {
--md-hue: 230;
--md-default-bg-color: hsla(230, 15%, 21%, 1);
--md-default-bg-color: #1A1A1A;
/* Background changed to dark gray */
}
.hide {
@@ -24,12 +26,15 @@ img.index-header {
max-width: 500px;
}
/* Updated custom colors */
.pydantic-pink {
color: #FF007F;
color: #E0A3B6;
/* Updated to match new theme */
}
.team-blue {
color: #0072CE;
color: #1C3F74;
/* Updated to match new theme */
}
.secure-green {
@@ -67,7 +72,6 @@ img.index-header {
text-align: center;
}
/* Hide the entire footer */
.md-footer {
display: none;
+1 -1
View File
@@ -89,7 +89,7 @@ theme:
name: Switch to light mode
icon:
repo: fontawesome/brands/github
favicon: "https://res.cloudinary.com/dq0w2rtm9/image/upload/v1737555066/r17hrkre246doczwmvbv.png"
favicon: https://res.cloudinary.com/dq0w2rtm9/image/upload/v1741195421/favicon_kuz6xr.png
extra:
generator: false
Generated
+307 -265
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -229,6 +229,24 @@ files = [
[package.extras]
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "backrefs"
version = "5.8"
description = "A wrapper around re and regex that adds additional back references."
optional = false
python-versions = ">=3.9"
files = [
{file = "backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d"},
{file = "backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b"},
{file = "backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486"},
{file = "backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585"},
{file = "backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc"},
{file = "backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd"},
]
[package.extras]
extras = ["regex"]
[[package]]
name = "beautifulsoup4"
version = "4.12.3"
@@ -768,6 +786,20 @@ files = [
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
]
[[package]]
name = "execnet"
version = "2.1.1"
description = "execnet: rapid multi-Python deployment"
optional = false
python-versions = ">=3.8"
files = [
{file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"},
{file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"},
]
[package.extras]
testing = ["hatch", "pre-commit", "pytest", "tox"]
[[package]]
name = "executing"
version = "2.2.0"
@@ -784,18 +816,18 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth
[[package]]
name = "fastapi"
version = "0.115.8"
version = "0.115.11"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
{file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"},
{file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"},
{file = "fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64"},
{file = "fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.40.0,<0.46.0"
starlette = ">=0.40.0,<0.47.0"
typing-extensions = ">=4.8.0"
[package.extras]
@@ -1202,13 +1234,13 @@ files = [
[[package]]
name = "inline-snapshot"
version = "0.20.1"
version = "0.20.3"
description = "golden master/snapshot/approval testing library which puts the values right into your source code"
optional = false
python-versions = ">=3.8"
files = [
{file = "inline_snapshot-0.20.1-py3-none-any.whl", hash = "sha256:5b5c3fd037f340dff5adee1c2c58db9038325937a8190dedbba98e37b87c979a"},
{file = "inline_snapshot-0.20.1.tar.gz", hash = "sha256:c56c871e59973500eca00610022eac19e79cd2c1b0b2d7a18abe14dde11a1431"},
{file = "inline_snapshot-0.20.3-py3-none-any.whl", hash = "sha256:1ea999fbf38dd11cc72d0e1a0b9303c63d496b77bdc406a394fe2424ae842f70"},
{file = "inline_snapshot-0.20.3.tar.gz", hash = "sha256:7a353170b7e42aa89086c7ba790a973c9645523acf985532648dabd7ee2d71f2"},
]
[package.dependencies]
@@ -1217,7 +1249,7 @@ executing = ">=2.2.0"
rich = ">=13.7.1"
[package.extras]
black = ["black (>=23.3.0)", "click (>=8.1.4)"]
black = ["black (>=23.3.0)"]
dirty-equals = ["dirty-equals (>=0.9.0)"]
[[package]]
@@ -1311,13 +1343,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"]
[[package]]
name = "jinja2"
version = "3.1.5"
version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
{file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
]
[package.dependencies]
@@ -1695,45 +1727,45 @@ files = [
[[package]]
name = "matplotlib"
version = "3.10.0"
version = "3.10.1"
description = "Python plotting package"
optional = false
python-versions = ">=3.10"
files = [
{file = "matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6"},
{file = "matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e"},
{file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5"},
{file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6"},
{file = "matplotlib-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1"},
{file = "matplotlib-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3"},
{file = "matplotlib-3.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fd44fc75522f58612ec4a33958a7e5552562b7705b42ef1b4f8c0818e304a363"},
{file = "matplotlib-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c58a9622d5dbeb668f407f35f4e6bfac34bb9ecdcc81680c04d0258169747997"},
{file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:845d96568ec873be63f25fa80e9e7fae4be854a66a7e2f0c8ccc99e94a8bd4ef"},
{file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5439f4c5a3e2e8eab18e2f8c3ef929772fd5641876db71f08127eed95ab64683"},
{file = "matplotlib-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4673ff67a36152c48ddeaf1135e74ce0d4bce1bbf836ae40ed39c29edf7e2765"},
{file = "matplotlib-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e8632baebb058555ac0cde75db885c61f1212e47723d63921879806b40bec6a"},
{file = "matplotlib-3.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4659665bc7c9b58f8c00317c3c2a299f7f258eeae5a5d56b4c64226fca2f7c59"},
{file = "matplotlib-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d44cb942af1693cced2604c33a9abcef6205601c445f6d0dc531d813af8a2f5a"},
{file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a994f29e968ca002b50982b27168addfd65f0105610b6be7fa515ca4b5307c95"},
{file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0558bae37f154fffda54d779a592bc97ca8b4701f1c710055b609a3bac44c8"},
{file = "matplotlib-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:503feb23bd8c8acc75541548a1d709c059b7184cde26314896e10a9f14df5f12"},
{file = "matplotlib-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:c40ba2eb08b3f5de88152c2333c58cee7edcead0a2a0d60fcafa116b17117adc"},
{file = "matplotlib-3.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96f2886f5c1e466f21cc41b70c5a0cd47bfa0015eb2d5793c88ebce658600e25"},
{file = "matplotlib-3.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:12eaf48463b472c3c0f8dbacdbf906e573013df81a0ab82f0616ea4b11281908"},
{file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fbbabc82fde51391c4da5006f965e36d86d95f6ee83fb594b279564a4c5d0d2"},
{file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2e15300530c1a94c63cfa546e3b7864bd18ea2901317bae8bbf06a5ade6dcf"},
{file = "matplotlib-3.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3547d153d70233a8496859097ef0312212e2689cdf8d7ed764441c77604095ae"},
{file = "matplotlib-3.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c55b20591ced744aa04e8c3e4b7543ea4d650b6c3c4b208c08a05b4010e8b442"},
{file = "matplotlib-3.10.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ade1003376731a971e398cc4ef38bb83ee8caf0aee46ac6daa4b0506db1fd06"},
{file = "matplotlib-3.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95b710fea129c76d30be72c3b38f330269363fbc6e570a5dd43580487380b5ff"},
{file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdbaf909887373c3e094b0318d7ff230b2ad9dcb64da7ade654182872ab2593"},
{file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d907fddb39f923d011875452ff1eca29a9e7f21722b873e90db32e5d8ddff12e"},
{file = "matplotlib-3.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3b427392354d10975c1d0f4ee18aa5844640b512d5311ef32efd4dd7db106ede"},
{file = "matplotlib-3.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5fd41b0ec7ee45cd960a8e71aea7c946a28a0b8a4dcee47d2856b2af051f334c"},
{file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03"},
{file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea"},
{file = "matplotlib-3.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef"},
{file = "matplotlib-3.10.0.tar.gz", hash = "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278"},
{file = "matplotlib-3.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16"},
{file = "matplotlib-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2"},
{file = "matplotlib-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0673b4b8f131890eb3a1ad058d6e065fb3c6e71f160089b65f8515373394698"},
{file = "matplotlib-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e875b95ac59a7908978fe307ecdbdd9a26af7fa0f33f474a27fcf8c99f64a19"},
{file = "matplotlib-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2589659ea30726284c6c91037216f64a506a9822f8e50592d48ac16a2f29e044"},
{file = "matplotlib-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a97ff127f295817bc34517255c9db6e71de8eddaab7f837b7d341dee9f2f587f"},
{file = "matplotlib-3.10.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:057206ff2d6ab82ff3e94ebd94463d084760ca682ed5f150817b859372ec4401"},
{file = "matplotlib-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a144867dd6bf8ba8cb5fc81a158b645037e11b3e5cf8a50bd5f9917cb863adfe"},
{file = "matplotlib-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56c5d9fcd9879aa8040f196a235e2dcbdf7dd03ab5b07c0696f80bc6cf04bedd"},
{file = "matplotlib-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f69dc9713e4ad2fb21a1c30e37bd445d496524257dfda40ff4a8efb3604ab5c"},
{file = "matplotlib-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c59af3e8aca75d7744b68e8e78a669e91ccbcf1ac35d0102a7b1b46883f1dd7"},
{file = "matplotlib-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:11b65088c6f3dae784bc72e8d039a2580186285f87448babb9ddb2ad0082993a"},
{file = "matplotlib-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:66e907a06e68cb6cfd652c193311d61a12b54f56809cafbed9736ce5ad92f107"},
{file = "matplotlib-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b4bb156abb8fa5e5b2b460196f7db7264fc6d62678c03457979e7d5254b7be"},
{file = "matplotlib-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1985ad3d97f51307a2cbfc801a930f120def19ba22864182dacef55277102ba6"},
{file = "matplotlib-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96f2c2f825d1257e437a1482c5a2cf4fee15db4261bd6fc0750f81ba2b4ba3d"},
{file = "matplotlib-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35e87384ee9e488d8dd5a2dd7baf471178d38b90618d8ea147aced4ab59c9bea"},
{file = "matplotlib-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfd414bce89cc78a7e1d25202e979b3f1af799e416010a20ab2b5ebb3a02425c"},
{file = "matplotlib-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b"},
{file = "matplotlib-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1"},
{file = "matplotlib-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3"},
{file = "matplotlib-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6"},
{file = "matplotlib-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b"},
{file = "matplotlib-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473"},
{file = "matplotlib-3.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01"},
{file = "matplotlib-3.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb"},
{file = "matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972"},
{file = "matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3"},
{file = "matplotlib-3.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f"},
{file = "matplotlib-3.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9"},
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:648406f1899f9a818cef8c0231b44dcfc4ff36f167101c3fd1c9151f24220fdc"},
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:02582304e352f40520727984a5a18f37e8187861f954fea9be7ef06569cf85b4"},
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3809916157ba871bcdd33d3493acd7fe3037db5daa917ca6e77975a94cef779"},
{file = "matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba"},
]
[package.dependencies]
@@ -1848,13 +1880,13 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
[[package]]
name = "mkdocs-autorefs"
version = "1.3.0"
version = "1.4.0"
description = "Automatically link across pages in MkDocs."
optional = false
python-versions = ">=3.9"
files = [
{file = "mkdocs_autorefs-1.3.0-py3-none-any.whl", hash = "sha256:d180f9778a04e78b7134e31418f238bba56f56d6a8af97873946ff661befffb3"},
{file = "mkdocs_autorefs-1.3.0.tar.gz", hash = "sha256:6867764c099ace9025d6ac24fd07b85a98335fbd30107ef01053697c8f46db61"},
{file = "mkdocs_autorefs-1.4.0-py3-none-any.whl", hash = "sha256:bad19f69655878d20194acd0162e29a89c3f7e6365ffe54e72aa3fd1072f240d"},
{file = "mkdocs_autorefs-1.4.0.tar.gz", hash = "sha256:a9c0aa9c90edbce302c09d050a3c4cb7c76f8b7b2c98f84a7a05f53d00392156"},
]
[package.dependencies]
@@ -1899,17 +1931,18 @@ pygments = ">2.12.0"
[[package]]
name = "mkdocs-material"
version = "9.6.4"
version = "9.6.7"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_material-9.6.4-py3-none-any.whl", hash = "sha256:414e8376551def6d644b8e6f77226022868532a792eb2c9accf52199009f568f"},
{file = "mkdocs_material-9.6.4.tar.gz", hash = "sha256:4d1d35e1c1d3e15294cb7fa5d02e0abaee70d408f75027dc7be6e30fb32e6867"},
{file = "mkdocs_material-9.6.7-py3-none-any.whl", hash = "sha256:8a159e45e80fcaadd9fbeef62cbf928569b93df954d4dc5ba76d46820caf7b47"},
{file = "mkdocs_material-9.6.7.tar.gz", hash = "sha256:3e2c1fceb9410056c2d91f334a00cdea3215c28750e00c691c1e46b2a33309b4"},
]
[package.dependencies]
babel = ">=2.10,<3.0"
backrefs = ">=5.7.post1,<6.0"
colorama = ">=0.4,<1.0"
jinja2 = ">=3.0,<4.0"
markdown = ">=3.2,<4.0"
@@ -1918,7 +1951,6 @@ mkdocs-material-extensions = ">=1.3,<2.0"
paginate = ">=0.5,<1.0"
pygments = ">=2.16,<3.0"
pymdown-extensions = ">=10.2,<11.0"
regex = ">=2022.4"
requests = ">=2.26,<3.0"
[package.extras]
@@ -1939,13 +1971,13 @@ files = [
[[package]]
name = "mkdocstrings"
version = "0.28.1"
version = "0.28.2"
description = "Automatic documentation from sources, for MkDocs."
optional = false
python-versions = ">=3.9"
files = [
{file = "mkdocstrings-0.28.1-py3-none-any.whl", hash = "sha256:a5878ae5cd1e26f491ff084c1f9ab995687d52d39a5c558e9b7023d0e4e0b740"},
{file = "mkdocstrings-0.28.1.tar.gz", hash = "sha256:fb64576906771b7701e8e962fd90073650ff689e95eb86e86751a66d65ab4489"},
{file = "mkdocstrings-0.28.2-py3-none-any.whl", hash = "sha256:57f79c557e2718d217d6f6a81bf75a0de097f10e922e7e5e00f085c3f0ff6895"},
{file = "mkdocstrings-0.28.2.tar.gz", hash = "sha256:9b847266d7a588ea76a8385eaebe1538278b4361c0d1ce48ed005be59f053569"},
]
[package.dependencies]
@@ -1953,7 +1985,7 @@ Jinja2 = ">=2.11.1"
Markdown = ">=3.6"
MarkupSafe = ">=1.1"
mkdocs = ">=1.4"
mkdocs-autorefs = ">=1.3"
mkdocs-autorefs = ">=1.4"
mkdocs-get-deps = ">=0.2"
pymdown-extensions = ">=6.3"
@@ -2087,49 +2119,43 @@ dill = ">=0.3.8"
[[package]]
name = "mypy"
version = "1.14.1"
version = "1.15.0"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"},
{file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"},
{file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"},
{file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"},
{file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"},
{file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"},
{file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"},
{file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"},
{file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"},
{file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"},
{file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"},
{file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"},
{file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"},
{file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"},
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"},
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"},
{file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"},
{file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"},
{file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"},
{file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"},
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"},
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"},
{file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"},
{file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"},
{file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"},
{file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"},
{file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"},
{file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"},
{file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"},
{file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"},
{file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"},
{file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"},
{file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"},
{file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"},
{file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"},
{file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"},
{file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"},
{file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"},
{file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"},
{file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"},
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"},
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"},
{file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"},
{file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"},
{file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"},
{file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"},
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"},
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"},
{file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"},
{file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"},
{file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"},
{file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"},
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"},
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"},
{file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"},
{file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"},
{file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"},
{file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"},
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"},
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"},
{file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"},
{file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"},
{file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"},
{file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"},
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"},
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"},
{file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"},
{file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"},
{file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"},
{file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"},
]
[package.dependencies]
@@ -2257,66 +2283,154 @@ files = [
[[package]]
name = "numpy"
version = "2.2.2"
version = "2.2.3"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.10"
files = [
{file = "numpy-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e"},
{file = "numpy-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e"},
{file = "numpy-2.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715"},
{file = "numpy-2.2.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a"},
{file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97"},
{file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957"},
{file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d"},
{file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd"},
{file = "numpy-2.2.2-cp310-cp310-win32.whl", hash = "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160"},
{file = "numpy-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014"},
{file = "numpy-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189"},
{file = "numpy-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323"},
{file = "numpy-2.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac"},
{file = "numpy-2.2.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e"},
{file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c"},
{file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f"},
{file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826"},
{file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8"},
{file = "numpy-2.2.2-cp311-cp311-win32.whl", hash = "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50"},
{file = "numpy-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2"},
{file = "numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467"},
{file = "numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a"},
{file = "numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825"},
{file = "numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37"},
{file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748"},
{file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0"},
{file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278"},
{file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba"},
{file = "numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283"},
{file = "numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb"},
{file = "numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc"},
{file = "numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369"},
{file = "numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd"},
{file = "numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be"},
{file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84"},
{file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff"},
{file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0"},
{file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de"},
{file = "numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9"},
{file = "numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369"},
{file = "numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391"},
{file = "numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39"},
{file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317"},
{file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49"},
{file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2"},
{file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7"},
{file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb"},
{file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648"},
{file = "numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4"},
{file = "numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576"},
{file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495"},
{file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df"},
{file = "numpy-2.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a"},
{file = "numpy-2.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60"},
{file = "numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"},
{file = "numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71"},
{file = "numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787"},
{file = "numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716"},
{file = "numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b"},
{file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3"},
{file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52"},
{file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b"},
{file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027"},
{file = "numpy-2.2.3-cp310-cp310-win32.whl", hash = "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094"},
{file = "numpy-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb"},
{file = "numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8"},
{file = "numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b"},
{file = "numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a"},
{file = "numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636"},
{file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d"},
{file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb"},
{file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2"},
{file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b"},
{file = "numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5"},
{file = "numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f"},
{file = "numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d"},
{file = "numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95"},
{file = "numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea"},
{file = "numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532"},
{file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e"},
{file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe"},
{file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021"},
{file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8"},
{file = "numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe"},
{file = "numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d"},
{file = "numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba"},
{file = "numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50"},
{file = "numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1"},
{file = "numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5"},
{file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2"},
{file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1"},
{file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304"},
{file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d"},
{file = "numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693"},
{file = "numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0"},
{file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610"},
{file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76"},
{file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a"},
{file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf"},
{file = "numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef"},
{file = "numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082"},
{file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d"},
{file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9"},
{file = "numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e"},
{file = "numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4"},
{file = "numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020"},
]
[[package]]
name = "orjson"
version = "3.10.15"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
files = [
{file = "orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e"},
{file = "orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab"},
{file = "orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806"},
{file = "orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c"},
{file = "orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e"},
{file = "orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e"},
{file = "orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a"},
{file = "orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665"},
{file = "orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa"},
{file = "orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825"},
{file = "orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890"},
{file = "orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf"},
{file = "orjson-3.10.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e8afd6200e12771467a1a44e5ad780614b86abb4b11862ec54861a82d677746"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9a18c500f19273e9e104cca8c1f0b40a6470bcccfc33afcc088045d0bf5ea6"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb00b7bfbdf5d34a13180e4805d76b4567025da19a197645ca746fc2fb536586"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33aedc3d903378e257047fee506f11e0833146ca3e57a1a1fb0ddb789876c1e1"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0099ae6aed5eb1fc84c9eb72b95505a3df4267e6962eb93cdd5af03be71c98"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c864a80a2d467d7786274fce0e4f93ef2a7ca4ff31f7fc5634225aaa4e9e98c"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c25774c9e88a3e0013d7d1a6c8056926b607a61edd423b50eb5c88fd7f2823ae"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e78c211d0074e783d824ce7bb85bf459f93a233eb67a5b5003498232ddfb0e8a"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:43e17289ffdbbac8f39243916c893d2ae41a2ea1a9cbb060a56a4d75286351ae"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:781d54657063f361e89714293c095f506c533582ee40a426cb6489c48a637b81"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6875210307d36c94873f553786a808af2788e362bd0cf4c8e66d976791e7b528"},
{file = "orjson-3.10.15-cp38-cp38-win32.whl", hash = "sha256:305b38b2b8f8083cc3d618927d7f424349afce5975b316d33075ef0f73576b60"},
{file = "orjson-3.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:5dd9ef1639878cc3efffed349543cbf9372bdbd79f478615a1c633fe4e4180d1"},
{file = "orjson-3.10.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ffe19f3e8d68111e8644d4f4e267a069ca427926855582ff01fc012496d19969"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d433bf32a363823863a96561a555227c18a522a8217a6f9400f00ddc70139ae2"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da03392674f59a95d03fa5fb9fe3a160b0511ad84b7a3914699ea5a1b3a38da2"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a63bb41559b05360ded9132032239e47983a39b151af1201f07ec9370715c82"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3766ac4702f8f795ff3fa067968e806b4344af257011858cc3d6d8721588b53f"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1c73dcc8fadbd7c55802d9aa093b36878d34a3b3222c41052ce6b0fc65f8e8"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b299383825eafe642cbab34be762ccff9fd3408d72726a6b2a4506d410a71ab3"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:abc7abecdbf67a173ef1316036ebbf54ce400ef2300b4e26a7b843bd446c2480"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3614ea508d522a621384c1d6639016a5a2e4f027f3e4a1c93a51867615d28829"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:295c70f9dc154307777ba30fe29ff15c1bcc9dfc5c48632f37d20a607e9ba85a"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:63309e3ff924c62404923c80b9e2048c1f74ba4b615e7584584389ada50ed428"},
{file = "orjson-3.10.15-cp39-cp39-win32.whl", hash = "sha256:a2f708c62d026fb5340788ba94a55c23df4e1869fec74be455e0b2f5363b8507"},
{file = "orjson-3.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:efcf6c735c3d22ef60c4aa27a5238f1a477df85e9b15f2142f9d669beb2d13fd"},
{file = "orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e"},
]
[[package]]
@@ -3014,6 +3128,17 @@ files = [
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pyfiglet"
version = "1.0.2"
description = "Pure-python FIGlet implementation"
optional = false
python-versions = ">=3.9"
files = [
{file = "pyfiglet-1.0.2-py3-none-any.whl", hash = "sha256:889b351d79c99e50a3f619c8f8e6ffdb27fd8c939fc43ecbd7559bd57d5f93ea"},
{file = "pyfiglet-1.0.2.tar.gz", hash = "sha256:758788018ab8faaddc0984e1ea05ff330d3c64be663c513cc1f105f6a3066dab"},
]
[[package]]
name = "pygments"
version = "2.18.0"
@@ -3062,13 +3187,13 @@ diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pytest"
version = "8.3.4"
version = "8.3.5"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
]
[package.dependencies]
@@ -3133,6 +3258,26 @@ pytest = ">=6.2.5"
[package.extras]
dev = ["pre-commit", "pytest-asyncio", "tox"]
[[package]]
name = "pytest-xdist"
version = "3.6.1"
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"},
{file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"},
]
[package.dependencies]
execnet = ">=2.1"
pytest = ">=7.0.0"
[package.extras]
psutil = ["psutil (>=3.0)"]
setproctitle = ["setproctitle"]
testing = ["filelock"]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -3408,109 +3553,6 @@ files = [
attrs = ">=22.2.0"
rpds-py = ">=0.7.0"
[[package]]
name = "regex"
version = "2024.11.6"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.8"
files = [
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"},
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"},
{file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"},
{file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"},
{file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"},
{file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"},
{file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"},
{file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"},
{file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"},
{file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"},
{file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"},
{file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"},
{file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"},
{file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"},
{file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"},
{file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"},
{file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"},
{file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"},
{file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"},
{file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"},
{file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"},
{file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"},
{file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"},
{file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"},
{file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"},
{file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"},
{file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"},
{file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"},
{file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"},
{file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"},
{file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"},
{file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"},
{file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"},
{file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"},
{file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"},
{file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"},
{file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"},
{file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"},
{file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"},
{file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"},
{file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"},
{file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"},
{file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"},
]
[[package]]
name = "requests"
version = "2.32.3"
@@ -3931,13 +3973,13 @@ widechars = ["wcwidth"]
[[package]]
name = "termcolor"
version = "2.4.0"
version = "2.5.0"
description = "ANSI color formatting for output in terminal"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"},
{file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"},
{file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"},
{file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"},
]
[package.extras]
@@ -4439,4 +4481,4 @@ propcache = ">=0.2.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "a741ff960d86175204b90cdb4f935d3873a6a38d2d547c1ded73c17ab54b4312"
content-hash = "35e03dba41d30cf6129a4a4f3107eca560f779205b21d3ffb2871eeffc5d5a64"
+8 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "agentic_security"
version = "0.5.0"
version = "0.6.0"
description = "Agentic LLM vulnerability scanner"
authors = ["Alexander Miasoiedov <msoedov@gmail.com>"]
maintainers = ["Alexander Miasoiedov <msoedov@gmail.com>"]
@@ -49,7 +49,12 @@ tomli = "^2.2.1"
rich = "13.9.4"
gTTS = "^2.5.4"
sentry_sdk = "^2.22.0"
orjson = "^3.10"
pyfiglet = "^1.0.2"
termcolor = "^2.4.0"
# garak = { version = "*", optional = true }
pytest-xdist = "3.6.1"
[tool.poetry.group.dev.dependencies]
@@ -82,5 +87,7 @@ build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
addopts = "--durations=5 -m 'not slow' -n auto"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
markers = "slow: marks tests as slow"
View File
+8
View File
@@ -0,0 +1,8 @@
import os
import pytest
def pytest_runtest_setup(item):
if "slow" in item.keywords and not os.getenv("RUN_SLOW_TESTS"):
pytest.skip("Skipping slow test")
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
import httpx
import pytest
from agentic_security.models.schemas import Scan
from agentic_security.primitives import Scan
from agentic_security.probe_actor.fuzzer import (
generate_prompts,
perform_many_shot_scan,
+13
View File
@@ -0,0 +1,13 @@
from agentic_security.probe_actor.refusal import DefaultRefusalClassifier
class TestCheckRefusal:
# The function correctly identifies a refusal phrase in the response.
def test_identify_refusal_phrase(self):
response = "I'm sorry, but I cannot provide that information."
assert DefaultRefusalClassifier().is_refusal(response)
# The response is an empty string.
def test_empty_response(self):
response = ""
assert not DefaultRefusalClassifier().is_refusal(response)
@@ -3,7 +3,7 @@ from unittest.mock import patch
import pandas as pd
import pytest
from .model import RefusalClassifier
from agentic_security.refusal_classifier.model import RefusalClassifier
@pytest.fixture
View File
@@ -1,6 +1,6 @@
from fastapi.testclient import TestClient
from ..app import app
from agentic_security.app import app
def test_health_check():
@@ -5,10 +5,10 @@ import httpx
import pytest
from fastapi.testclient import TestClient
from ..app import app
from ..models.schemas import Probe
from ..probe_actor.refusal import REFUSAL_MARKS
from ..probe_data import REGISTRY
from agentic_security.app import app
from agentic_security.primitives import Probe
from agentic_security.probe_actor.refusal import REFUSAL_MARKS
from agentic_security.probe_data import REGISTRY
client = TestClient(app)
@@ -4,7 +4,7 @@ from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
from .report import router
from agentic_security.routes.report import router
client = TestClient(router)
@@ -4,8 +4,8 @@ import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from ..models.schemas import Settings
from .static import get_static_file, router
from agentic_security.primitives import Settings
from agentic_security.routes.static import get_static_file, router
client = TestClient(router)
+26
View File
@@ -0,0 +1,26 @@
import pytest
from datasets import load_dataset
from agentic_security.probe_data import REGISTRY
@pytest.mark.slow
@pytest.mark.parametrize("dataset", REGISTRY)
def test_registry_accessibility(dataset):
source = dataset.get("source", "")
if "hugging" not in source.lower():
return pytest.skip("skipped dataset")
if not dataset.get("is_active"):
return pytest.skip("skipped dataset")
dataset_name = dataset.get("dataset_name")
if not dataset_name:
pytest.fail(f"No dataset_name found in {dataset}")
# Load only metadata (no data download)
try:
ds = load_dataset(dataset_name, split=None)
# Check if metadata is accessible without loading full data
assert ds is not None, f"Failed to load metadata for {dataset_name}"
except Exception as e:
pytest.fail(f"Error loading metadata for {dataset_name}: {str(e)}")
+136
View File
@@ -0,0 +1,136 @@
import io
import string
import pytest
import pandas as pd
import numpy as np
from agentic_security.report_chart import plot_security_report, generate_identifiers
class TestReportChart:
"""Test suite for agentic_security.report_chart module."""
def test_generate_identifiers_short(self):
"""Test generate_identifiers with a small dataset."""
df = pd.DataFrame([{'dummy': i} for i in range(5)])
identifiers = generate_identifiers(df)
expected = ['A1', 'A2', 'A3', 'A4', 'A5']
assert identifiers == expected
def test_generate_identifiers_edge(self):
"""Test generate_identifiers with more than 26 items to cover cycling over the alphabet."""
n = 30
df = pd.DataFrame([{'dummy': i} for i in range(n)])
identifiers = generate_identifiers(df)
# For i=25, identifier should be A26, and for i=26, identifier should be B1
assert identifiers[25] == 'A26'
assert identifiers[26] == 'B1'
assert len(identifiers) == n
def test_generate_identifiers_empty(self):
"""Test generate_identifiers with an empty dataframe."""
df = pd.DataFrame([])
identifiers = generate_identifiers(df)
assert identifiers == []
def test_plot_security_report_png_output(self):
"""Test plot_security_report returns valid PNG output."""
# Create a sample table with required columns
table = [
{"failureRate": 10, "tokens": 100, "module": "Module1"},
{"failureRate": 30, "tokens": 200, "module": "Module2"},
{"failureRate": 20, "tokens": 150, "module": "Module3"},
]
buf = plot_security_report(table)
# Check that buf is a BytesIO object and starts with PNG header bytes
assert isinstance(buf, io.BytesIO)
buf.seek(0)
header = buf.read(8)
assert header.startswith(b'\x89PNG')
def test_plot_security_report_ordering(self, monkeypatch):
"""Test that the table embedded in the plot contains correctly sorted order by descending failure rate."""
table = [
{"failureRate": 15, "tokens": 110, "module": "ModuleA"},
{"failureRate": 25, "tokens": 210, "module": "ModuleB"},
{"failureRate": 5, "tokens": 90, "module": "ModuleC"},
]
result_holder = {}
from matplotlib.axes import Axes
original_table = Axes.table
def fake_table(self, *args, **kwargs):
result_holder['cellText'] = kwargs.get('cellText')
return original_table(self, *args, **kwargs)
monkeypatch.setattr(Axes, "table", fake_table)
plot_security_report(table)
cell_text = result_holder.get('cellText')
assert cell_text is not None
# Verify header row in the table
assert cell_text[0] == ["Threat"]
# Since the data are sorted (highest failure rate first), ModuleB (25.0%) should appear in one of the rows.
found = any("ModuleB (25.0%)" in row[0] for row in cell_text[1:])
assert found
def test_plot_security_report_one_entry(self):
"""Test plot_security_report with a single entry."""
table = [{"failureRate": 50, "tokens": 300, "module": "OnlyModule"}]
buf = plot_security_report(table)
assert isinstance(buf, io.BytesIO)
buf.seek(0)
content = buf.read()
assert content.startswith(b'\x89PNG')
def test_generate_identifiers_many(self):
"""Test generate_identifiers with 52 items to verify identifier sequence."""
n = 52
df = pd.DataFrame([{'dummy': i} for i in range(n)])
identifiers = generate_identifiers(df)
assert identifiers[0] == "A1"
assert identifiers[25] == "A26"
assert identifiers[26] == "B1"
assert identifiers[51] == "B26"
def test_plot_security_report_missing_failureRate(self):
"""Test plot_security_report raises KeyError when 'failureRate' column is missing."""
table = [{"tokens": 100, "module": "Mod1"}] # Missing 'failureRate'
with pytest.raises(KeyError):
plot_security_report(table)
def test_plot_security_report_missing_tokens(self):
"""Test plot_security_report raises KeyError when 'tokens' column is missing."""
table = [{"failureRate": 10, "module": "Mod1"}] # Missing 'tokens'
with pytest.raises(KeyError):
plot_security_report(table)
def test_plot_security_report_empty_table(self):
"""Test plot_security_report raises KeyError when the table is empty."""
table = []
with pytest.raises(KeyError):
plot_security_report(table)
def test_plot_security_report_missing_module(self):
"""Test plot_security_report raises KeyError when 'module' column is missing."""
table = [{"failureRate": 10, "tokens": 100}] # Missing 'module'
with pytest.raises(KeyError):
plot_security_report(table)
def test_plot_security_report_failure_rate_labels(self, monkeypatch):
"""Test that plot_security_report calls ax.text for each failure rate bar label."""
table = [
{"failureRate": 10, "tokens": 100, "module": "Mod1"},
{"failureRate": 20, "tokens": 150, "module": "Mod2"},
{"failureRate": 30, "tokens": 200, "module": "Mod3"},
]
# Count the number of times ax.text is called for drawing failure rate labels.
call_count = [0]
from matplotlib.axes import Axes
original_text = Axes.text
def fake_text(self, *args, **kwargs):
call_count[0] += 1
return original_text(self, *args, **kwargs)
monkeypatch.setattr(Axes, "text", fake_text)
plot_security_report(table)
# The loop inside plot_security_report calls ax.text once for each data point.
assert call_count[0] == len(table)
def test_plot_security_report_non_numeric_failureRate(self):
"""Test that plot_security_report raises an exception when failureRate is non-numeric."""
table = [{"failureRate": "invalid", "tokens": 100, "module": "ModX"}]
with pytest.raises(Exception):
plot_security_report(table)
+126
View File
@@ -0,0 +1,126 @@
import io
import asyncio
import json
from datetime import datetime, timedelta
from threading import Event
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from agentic_security.routes import scan
# Dummy LLMSpec for success tests
class DummyLLMSpec:
def __init__(self, spec_string):
self.spec_string = spec_string
async def verify(self):
class DummyResponse:
status_code = 200
text = "verification succeeded"
elapsed = timedelta(seconds=0.5)
return DummyResponse()
@classmethod
def from_string(cls, spec_string):
return DummyLLMSpec(spec_string)
# Dummy scan_router generator to simulate streaming responses
async def dummy_scan_router(request_factory, scan_parameters, tools_inbox, stop_event):
for i in range(2):
yield f"result {i}"
# Define a dummy Secrets class for testing purposes.
class DummySecrets:
def __init__(self):
self.secrets = {}
# Create FastAPI app for testing and include the scan router.
@pytest.fixture
def app():
app = FastAPI()
app.include_router(scan.router)
return app
@pytest.fixture
def client(app):
return TestClient(app)
@pytest.fixture(autouse=True)
def patch_dependencies(monkeypatch):
# Patch LLMSpec used in the routes with our dummy implementation.
monkeypatch.setattr(scan, "LLMSpec", DummyLLMSpec)
# Patch fuzzer.scan_router to use our dummy scanning generator.
monkeypatch.setattr(scan.fuzzer, "scan_router", dummy_scan_router)
# Patch get_stop_event to return a dummy Event.
dummy_event = Event()
monkeypatch.setattr(scan, "get_stop_event", lambda: dummy_event)
# Patch get_tools_inbox to return None.
monkeypatch.setattr(scan, "get_tools_inbox", lambda: None)
# Patch set_current_run to be a no-op.
monkeypatch.setattr(scan, "set_current_run", lambda x: None)
# Patch get_in_memory_secrets to return a DummySecrets instance.
monkeypatch.setattr(scan, "get_in_memory_secrets", lambda: DummySecrets())
# Ensure Scan.with_secrets is a no-op if not already implemented.
if not hasattr(scan.Scan, "with_secrets"):
monkeypatch.setattr(scan.Scan, "with_secrets", lambda self, secrets: None)
def test_verify_success(client):
"""Test /verify endpoint for a successful verification."""
data = {"spec": "dummy"}
response = client.post("/verify", json=data)
res_json = response.json()
assert response.status_code == 200
assert res_json["status_code"] == 200
assert res_json["body"] == "verification succeeded"
assert "elapsed" in res_json
assert "timestamp" in res_json
def test_verify_failure(client, monkeypatch):
"""Test /verify endpoint when verification fails."""
class DummyLLMSpecFailure:
def __init__(self, spec_string):
self.spec_string = spec_string
async def verify(self):
raise Exception("verification error")
@classmethod
def from_string(cls, spec_string):
return DummyLLMSpecFailure(spec_string)
monkeypatch.setattr(scan, "LLMSpec", DummyLLMSpecFailure)
data = {"spec": "bad"}
response = client.post("/verify", json=data)
assert response.status_code == 400
assert "verification error" in response.text
def test_scan(client):
"""Test /scan endpoint to ensure streaming response works."""
data = {"llmSpec": "dummy", "optimize": False, "maxBudget": 10, "enableMultiStepAttack": False}
response = client.post("/scan", json=data)
assert response.status_code == 200
content = list(response.iter_lines())
expected = ["result 0", "result 1"]
assert content == expected
def test_stop_scan(client):
"""Test /stop endpoint to ensure scan stopping functionality."""
dummy_event = scan.get_stop_event()
dummy_event.clear()
response = client.post("/stop")
assert response.status_code == 200
assert response.json() == {"status": "Scan stopped"}
assert dummy_event.is_set()
def test_scan_csv(client):
"""Test /scan-csv endpoint with CSV file and llmSpec upload."""
csv_content = b"col1,col2\nvalue1,value2"
llm_spec_content = b"dummy"
files = {
"file": ("dummy.csv", csv_content, "text/csv"),
"llmSpec": ("spec.txt", llm_spec_content, "text/plain"),
}
response = client.post(
"/scan-csv",
files=files,
data={"optimize": "false", "maxBudget": "10", "enableMultiStepAttack": "false"},
)
assert response.status_code == 200
content = list(response.iter_lines())
expected = ["result 0", "result 1"]
assert content == expected