Compare commits

..

3 Commits

Author SHA1 Message Date
codebeaver-ai[bot] e752ebaeeb Adding codebeaver.yml 2025-03-03 18:41:09 +00:00
codebeaver-ai[bot] 2549194bd1 test: Add coverage improvement test for tests/test_http_spec.py 2025-03-03 18:41:08 +00:00
codebeaver-ai[bot] 4c580ea1b8 test: Add coverage improvement test for tests/test_app.py 2025-03-03 18:41:06 +00:00
70 changed files with 1754 additions and 2530 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ on:
- 0.* - 0.*
env: env:
POETRY_VERSION: "1.8.5" POETRY_VERSION: "1.7.1"
jobs: jobs:
if_release: if_release:
+1 -1
View File
@@ -7,7 +7,7 @@ on:
branches: [main] branches: [main]
env: env:
POETRY_VERSION: "1.8.5" POETRY_VERSION: "1.7.1"
OPENAI_API_KEY: "sk-fake" OPENAI_API_KEY: "sk-fake"
jobs: jobs:
-2
View File
@@ -17,5 +17,3 @@ inv/
scripts/ scripts/
docx/ docx/
agentic_security.toml agentic_security.toml
/venv
*.csv
+1 -4
View File
@@ -1,5 +1,5 @@
# Build stage # Build stage
FROM python:3.11-slim AS builder FROM python:3.11-slim as builder
WORKDIR /app WORKDIR /app
@@ -14,9 +14,6 @@ RUN poetry self add "poetry-plugin-export"
# Copy only dependency files to leverage Docker layer caching # Copy only dependency files to leverage Docker layer caching
COPY pyproject.toml poetry.lock ./ COPY pyproject.toml poetry.lock ./
# update lock file to avoid failure
RUN poetry lock
# Install dependencies # Install dependencies
RUN poetry export -f requirements.txt --without-hashes -o requirements.txt RUN poetry export -f requirements.txt --without-hashes -o requirements.txt
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
+5 -26
View File
@@ -83,7 +83,7 @@ agentic_security --port=PORT --host=HOST
## UI 🧙 ## UI 🧙
<img width="100%" alt="booking-screen" src="https://raw.githubusercontent.com/msoedov/agentic_security/refs/heads/main/docs/images/demo.gif"> <img width="100%" alt="booking-screen" src="https://res.cloudinary.com/dq0w2rtm9/image/upload/v1736433557/z0bsyzhsqlgcr3w4ovwp.gif">
## LLM kwargs ## LLM kwargs
@@ -402,31 +402,16 @@ This setup ensures a continuous integration approach towards maintaining securit
The `Module` class is designed to manage prompt processing and interaction with external AI models and tools. It supports fetching, processing, and posting prompts asynchronously for model vulnerabilities. Check out [module.md](https://github.com/msoedov/agentic_security/blob/main/docs/module.md) for details. The `Module` class is designed to manage prompt processing and interaction with external AI models and tools. It supports fetching, processing, and posting prompts asynchronously for model vulnerabilities. Check out [module.md](https://github.com/msoedov/agentic_security/blob/main/docs/module.md) for details.
## MCP server
```shell
pip install -U mcp
# From cloned directory
mcp install agentic_security/mcp/main.py
```
## Documentation ## Documentation
For more detailed information on how to use Agentic Security, including advanced features and customization options, please refer to the official documentation. For more detailed information on how to use Agentic Security, including advanced features and customization options, please refer to the official documentation.
## Roadmap and Future Goals ## Roadmap and Future Goals
- \[ \] Expand dataset variety
- \[ \] Introduce two new attack vectors
Were just getting started! Heres whats on the horizon: - \[ \] Develop initial attacker LLM
- \[ \] Complete integration of OWASP Top 10 classification
- **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 | | Tool | Source | Integrated |
|-------------------------|-------------------------------------------------------------------------------|------------| |-------------------------|-------------------------------------------------------------------------------|------------|
@@ -438,7 +423,6 @@ Were just getting started! Heres whats on the horizon:
Note: All dates are tentative and subject to change based on project progress and priorities. Note: All dates are tentative and subject to change based on project progress and priorities.
## 👋 Contributing ## 👋 Contributing
Contributions to Agentic Security are welcome! If you'd like to contribute, please follow these steps: Contributions to Agentic Security are welcome! If you'd like to contribute, please follow these steps:
@@ -455,9 +439,4 @@ Before contributing, please read the contributing guidelines.
Agentic Security is released under the Apache License v2. 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 ## Contact us
+2 -2
View File
@@ -1,3 +1,3 @@
from .lib import SecurityScanner from .lib import AgenticSecurity
__all__ = ["SecurityScanner"] __all__ = ["AgenticSecurity"]
+4 -4
View File
@@ -5,7 +5,7 @@ import fire
import uvicorn import uvicorn
from agentic_security.app import app from agentic_security.app import app
from agentic_security.lib import SecurityScanner from agentic_security.lib import AgenticSecurity
from agentic_security.misc.banner import init_banner from agentic_security.misc.banner import init_banner
@@ -32,14 +32,14 @@ class CLI:
Run Agentic Security in CI mode. Run Agentic Security in CI mode.
""" """
sys.path.append(os.path.dirname(".")) sys.path.append(os.path.dirname("."))
SecurityScanner().entrypoint() AgenticSecurity().entrypoint()
def init(self, host: str = "0.0.0.0", port: int = 8718): def init(self, host: str = "0.0.0.0", port: int = 8718):
""" """
Generate the default CI configuration file. Generate the default CI configuration file.
""" """
sys.path.append(os.path.dirname(".")) sys.path.append(os.path.dirname("."))
SecurityScanner().generate_default_settings(host, port) AgenticSecurity().generate_default_settings(host, port)
i = init i = init
@@ -48,7 +48,7 @@ class CLI:
List all available security checks. List all available security checks.
""" """
sys.path.append(os.path.dirname(".")) sys.path.append(os.path.dirname("."))
SecurityScanner().list_checks() AgenticSecurity().list_checks()
def main(): def main():
+3 -1
View File
@@ -1,4 +1,5 @@
import asyncio import asyncio
import logging
import os import os
from typing import Any from typing import Any
@@ -9,11 +10,12 @@ from pydantic import BaseModel, ConfigDict, Field
# Assuming LLMSpec is defined elsewhere; placeholder import # Assuming LLMSpec is defined elsewhere; placeholder import
from agentic_security.http_spec import LLMSpec from agentic_security.http_spec import LLMSpec
from agentic_security.logutils import logger
LLM_SPECS = [] # Populate with LLM spec strings if needed LLM_SPECS = [] # Populate with LLM spec strings if needed
# Configure logging # Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Define AgentSpecification model # Define AgentSpecification model
+5 -1
View File
@@ -1,4 +1,5 @@
import asyncio import asyncio
import logging
from typing import Any from typing import Any
import httpx import httpx
@@ -7,10 +8,13 @@ from pydantic_ai import Agent, RunContext, Tool
# Assuming LLMSpec is defined elsewhere; placeholder import # Assuming LLMSpec is defined elsewhere; placeholder import
from agentic_security.http_spec import LLMSpec from agentic_security.http_spec import LLMSpec
from agentic_security.logutils import logger
LLM_SPECS = [] # Populate this list with LLM spec strings if needed 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 # Define AgentSpecification model
class AgentSpecification(BaseModel): class AgentSpecification(BaseModel):
+2 -10
View File
@@ -1,10 +1,9 @@
from functools import lru_cache from functools import lru_cache
import tomli import tomli
from loguru import logger
from agentic_security.logutils import logger SETTINGS_VERSION = 1
SETTINGS_VERSION = 2
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
@@ -143,13 +142,6 @@ use_disk_cache = false
retry = 3 retry = 3
timeout_connect = 30 timeout_connect = 30
timeout_response = 90 timeout_response = 90
[fuzzer]
max_prompt_lenght = 2048
budget_multiplier = 100000000
initial_optimizer_points = 25
min_failure_samples = 5
failure_rate_threshold = 0.5
""".replace( """.replace(
"$HOST", host "$HOST", host
) )
+6 -10
View File
@@ -4,14 +4,10 @@ from asyncio import Event, Queue
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from agentic_security.http_spec import LLMSpec
tools_inbox: Queue = Queue() tools_inbox: Queue = Queue()
stop_event: Event = Event() stop_event: Event = Event()
current_run: str = {"spec": "", "id": ""} current_run: str = {"spec": "", "id": ""}
_secrets: dict[str, str] = {} _secrets = {}
current_run: dict[str, int | LLMSpec] = {"spec": "", "id": ""}
def create_app() -> FastAPI: def create_app() -> FastAPI:
@@ -30,29 +26,29 @@ def get_stop_event() -> Event:
return stop_event return stop_event
def get_current_run() -> dict[str, int | LLMSpec]: def get_current_run() -> str:
"""Get the current run id.""" """Get the current run id."""
return current_run return current_run
def set_current_run(spec: LLMSpec) -> dict[str, int | LLMSpec]: def set_current_run(spec):
"""Set the current run id.""" """Set the current run id."""
current_run["id"] = hash(id(spec)) current_run["id"] = hash(id(spec))
current_run["spec"] = spec current_run["spec"] = spec
return current_run return current_run
def get_secrets() -> dict[str, str]: def get_secrets():
return _secrets return _secrets
def set_secrets(secrets: dict[str, str]) -> dict[str, str]: def set_secrets(secrets):
_secrets.update(secrets) _secrets.update(secrets)
expand_secrets(_secrets) expand_secrets(_secrets)
return _secrets return _secrets
def expand_secrets(secrets: dict[str, str]) -> None: def expand_secrets(secrets):
for key in secrets: for key in secrets:
val = secrets[key] val = secrets[key]
if val.startswith("$"): if val.startswith("$"):
+23 -2
View File
@@ -1,5 +1,26 @@
from agentic_security.logutils import set_log_level_to_info from logging import config
def setup_logging(): def setup_logging():
return set_log_level_to_info() config.dictConfig(
{
"version": 1,
"disable_existing_loggers": True,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
"loggers": {
"uvicorn.access": {
"level": "ERROR", # Set higher log level to suppress info logs globally
"handlers": ["console"],
"propagate": False,
}
},
}
)
-9
View File
@@ -1,6 +1,5 @@
import base64 import base64
from enum import Enum from enum import Enum
from urllib.parse import urlparse
import httpx import httpx
from pydantic import BaseModel from pydantic import BaseModel
@@ -160,14 +159,6 @@ def parse_http_spec(http_spec: str) -> LLMSpec:
# Extract the method and URL from the first line # Extract the method and URL from the first line
method, url = lines[0].split(" ")[0:2] method, url = lines[0].split(" ")[0:2]
# Check url validity
valid_url = urlparse(url)
# if missing the correct formatting ://, urlparse.netloc will be empty
if valid_url.scheme not in ("http", "https") or not valid_url.netloc:
raise InvalidHTTPSpecError(
f"Invalid URL: {url}. Ensure it starts with 'http://' or 'https://'"
)
# Initialize headers and body # Initialize headers and body
headers = {} headers = {}
body = "" body = ""
+3 -3
View File
@@ -4,12 +4,12 @@ from datetime import datetime
import colorama import colorama
import tqdm.asyncio import tqdm.asyncio
from loguru import logger
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
from tabulate import tabulate from tabulate import tabulate
from agentic_security.config import SettingsMixin # Importing the configuration mixin from agentic_security.config import SettingsMixin # Importing the configuration mixin
from agentic_security.logutils import logger
from agentic_security.primitives import Scan from agentic_security.primitives import Scan
from agentic_security.probe_data import REGISTRY from agentic_security.probe_data import REGISTRY
from agentic_security.routes.scan import streaming_response_generator from agentic_security.routes.scan import streaming_response_generator
@@ -23,7 +23,7 @@ YELLOW = colorama.Fore.YELLOW
BLUE = colorama.Fore.BLUE BLUE = colorama.Fore.BLUE
class SecurityScanner(SettingsMixin): class AgenticSecurity(SettingsMixin):
@classmethod @classmethod
async def async_scan( async def async_scan(
cls, cls,
@@ -202,7 +202,7 @@ class SecurityScanner(SettingsMixin):
self.load_config(self.default_path) self.load_config(self.default_path)
logger.info("Configuration loaded successfully.") logger.info("Configuration loaded successfully.")
print(self.config) print(self.config)
datasets = list(self.get_config_value("modules", {}).values()) datasets = list(self.get_config_value("modules").values())
for d in datasets: for d in datasets:
d["selected"] = True d["selected"] = True
self.scan( self.scan(
-146
View File
@@ -1,146 +0,0 @@
# import sys
# from loguru import logger
# # Define custom colors
# BLUE = "#89CFF0"
# BROWN = "#8B4513" # Brown for DEBUG
# # Define custom log level colors
# logger.level("DEBUG", color=f"<fg {BROWN}>")
# logger.level("INFO", color=f"<fg {BLUE}>")
# # Define custom log format with aligned messages and colored levels
# LOG_FORMAT = (
# "<level>{level:<8}</level> " # Properly formatted and colored log level
# "<level>{message:<100}</level> " # Left-aligned message for readability
# "<cyan>{file.name}</cyan>:<cyan>{line}</cyan>" # File name and line number in cyan
# )
# # Remove default handlers and add a new one with custom formatting
# logger.remove()
# logger.add(sys.stdout, format=LOG_FORMAT, level="DEBUG", colorize=True)
import logging
import logging.config
import time
from collections.abc import Callable, Coroutine
from functools import wraps
from os import getenv
from typing import Any, ParamSpec, TypeVar
LOGGER_NAME = None
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"rich": {"format": "%(message)s", "datefmt": "[%X]"},
},
"handlers": {
"rich": {
"class": "rich.logging.RichHandler",
"level": "INFO",
"formatter": "rich",
"show_time": False,
"rich_tracebacks": False,
"show_path": lambda: True if getenv("API_RUNTIME") == "dev" else False,
"tracebacks_show_locals": False,
},
},
"loggers": {
"": { # Root logger configuration
"level": "INFO",
"handlers": ["rich"],
"propagate": True,
},
"httpx": { # Disable httpx logging
"level": "WARNING", # Suppress DEBUG and INFO messages from httpx
"handlers": [],
"propagate": False,
},
"uvicorn.access": { # Disable uvicorn.access logging
"level": "WARNING", # Suppress DEBUG and INFO messages from uvicorn.access
"handlers": [],
"propagate": False,
},
},
}
def configure_logging():
# Apply the dictionary configuration
logging.config.dictConfig(LOGGING_CONFIG)
# Get and return the logger
logger = logging.getLogger(LOGGER_NAME)
return logger
logger: logging.Logger = configure_logging()
def set_log_level_to_debug():
logger = logging.getLogger(LOGGER_NAME)
logger.setLevel(logging.DEBUG)
# Update handler level as well
for handler in logger.handlers:
handler.setLevel(logging.DEBUG)
def set_log_level_to_info():
logger = logging.getLogger(LOGGER_NAME)
logger.setLevel(logging.INFO)
# Update handler level as well
for handler in logger.handlers:
handler.setLevel(logging.INFO)
# Set initial log level
set_log_level_to_info()
# Define generic type variables for return type and parameters
R = TypeVar("R")
P = ParamSpec("P")
def time_execution_sync(
additional_text: str = "",
) -> Callable[[Callable[P, R]], Callable[P, R]]:
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start_time = time.time()
result = func(*args, **kwargs)
execution_time = time.time() - start_time
logger.debug(
f"{additional_text} Execution time: {execution_time:.2f} seconds"
)
return result
return wrapper
return decorator
def time_execution_async(
additional_text: str = "",
) -> Callable[
[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]
]:
def decorator(
func: Callable[P, Coroutine[Any, Any, R]]
) -> Callable[P, Coroutine[Any, Any, R]]:
@wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start_time = time.time()
result = await func(*args, **kwargs)
execution_time = time.time() - start_time
logger.debug(
f"{additional_text} Execution time: {execution_time:.2f} seconds"
)
return result
return wrapper
return decorator
View File
-54
View File
@@ -1,54 +0,0 @@
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
# Create server parameters for stdio connection
server_params = StdioServerParameters(
command="python", # Executable
args=["agentic_security/mcp/main.py"], # Your server script
env=None, # Optional environment variables
)
async def run() -> None:
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# Initialize the connection --> connection does not work
await session.initialize()
# List available prompts, resources, and tools --> no avalialbe tools
prompts = await session.list_prompts()
print(f"Available prompts: {prompts}")
resources = await session.list_resources()
print(f"Available resources: {resources}")
tools = await session.list_tools()
print(f"Available tools: {tools}")
# Call the echo tool --> echo tool iisue
echo_result = await session.call_tool(
"echo_tool", arguments={"message": "Hello from client!"}
)
print(f"Tool result: {echo_result}")
# # Read the echo resource
# echo_content, mime_type = await session.read_resource(
# "echo://Hello_resource"
# )
# print(f"Resource content: {echo_content}")
# print(f"Resource MIME type: {mime_type}")
# # Get and use the echo prompt
# prompt_result = await session.get_prompt(
# "echo_prompt", arguments={"message": "Hello prompt!"}
# )
# print(f"Prompt result: {prompt_result}")
# You can perform additional operations here as needed
return prompts, resources, tools
if __name__ == "__main__":
asyncio.run(run())
-109
View File
@@ -1,109 +0,0 @@
import httpx
from mcp.server.fastmcp import FastMCP
# Initialize MCP server
mcp = FastMCP(
name="Agentic Security MCP Server",
description="MCP server to interact with LLM scanning test",
dependencies=["httpx"],
)
# FastAPI Server Configuration
AGENTIC_SECURITY = "http://0.0.0.0:8718"
@mcp.tool()
async def verify_llm(spec: str) -> dict:
"""
Verify an LLM model specification using the FastAPI server
Returns:
dict: containing the verification result form the FastAPI server
Args: spect(str): The specification of the LLM model to verify.
"""
url = f"{AGENTIC_SECURITY}/verify"
async with httpx.AsyncClient() as client:
response = await client.post(url, json={"spec": spec})
return response.json()
@mcp.tool()
async def start_scan(
llmSpec: str,
maxBudget: int,
optimize: bool = False,
enableMultiStepAttack: bool = False,
) -> dict:
"""
Start an LLM security scan via the FastAPI server.
Returns:
dict: The scan initiation result from the FastAPI server.
Args:
llmSpec (str): The specification of the LLM model.
maxBudget (int): The maximum budget for the scan.
optimize (bool, optional): Whether to enable optimization during scanning. Defaults to False.
enableMultiStepAttack (bool, optional): Whether to enable multi-step attack
"""
url = f"{AGENTIC_SECURITY}/scan"
payload = {
"llmSpec": llmSpec,
"maxBudget": maxBudget,
"datasets": [],
"optimize": optimize,
"enableMultiStepAttack": enableMultiStepAttack,
"probe_datasets": [],
"secrets": {},
}
async with httpx.AsyncClient() as client:
response = await client.post(url, json=payload)
return response.json()
@mcp.tool()
async def stop_scan() -> dict:
"""Stop an ongoing scan via the FastAPI server.
Returns:
dict: The confirmation from the FastAPI server that the scan has been stopped.
"""
url = f"{AGENTIC_SECURITY}/stop"
async with httpx.AsyncClient() as client:
response = await client.post(url)
return response.json()
@mcp.tool()
async def get_data_config() -> list:
"""
Retrieve data configuration from the FastAPI server.
Returns:
list: The response from the FastAPI server, confirming the scan has been stopped.
"""
url = f"{AGENTIC_SECURITY}/v1/data-config"
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()
@mcp.tool()
async def get_spec_templates() -> list:
"""
Retrieve data configuration from the FastAPI server.
Returns:
list: The LLM specification templates from the FastAPI server.
"""
url = f"{AGENTIC_SECURITY}/v1/llm-specs"
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()
# Run the MCP server
if __name__ == "__main__":
mcp.run()
+1 -2
View File
@@ -1,8 +1,7 @@
from fastapi import Request from fastapi import Request
from loguru import logger
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from agentic_security.logutils import logger
class LogNon200ResponsesMiddleware(BaseHTTPMiddleware): class LogNon200ResponsesMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next): async def dispatch(self, request: Request, call_next):
+8 -13
View File
@@ -8,14 +8,14 @@ except ImportError:
def generate_banner( def generate_banner(
title: str = "Agentic Security", title="Agentic Security",
font: str = "slant", font="slant",
version: str = "v2.1.0", version="v2.1.0",
tagline: str = "Proactive Threat Detection & Automated Security Protocols", tagline="Proactive Threat Detection & Automated Security Protocols",
author: str = "Developed by: [Security Team]", author="Developed by: [Security Team]",
website: str = "Website: https://github.com/msoedov/agentic_security", website="Website: https://github.com/msoedov/agentic_security",
warning: str | None = "", # Using Optional for warning since it might be None warning="",
) -> str: ):
"""Generate a visually enhanced banner with dynamic width and borders.""" """Generate a visually enhanced banner with dynamic width and borders."""
# Define the text elements # Define the text elements
@@ -80,13 +80,8 @@ def generate_banner(
def init_banner(): def init_banner():
return
ver = version("agentic_security") ver = version("agentic_security")
try:
print(generate_banner(version=ver)) print(generate_banner(version=ver))
except Exception:
# UnicodeEncodeError with codec on some systems
pass
if __name__ == "__main__": if __name__ == "__main__":
+1 -2
View File
@@ -1,6 +1,5 @@
# noqa
from agentic_security.primitives.models import CompletionRequest # noqa
from agentic_security.primitives.models import ( # noqa from agentic_security.primitives.models import ( # noqa
CompletionRequest,
FileProbeResponse, FileProbeResponse,
LLMInfo, LLMInfo,
Message, Message,
+122 -326
View File
@@ -3,44 +3,32 @@ import random
import time import time
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from json import JSONDecodeError from json import JSONDecodeError
from typing import Any
import httpx import httpx
import pandas as pd
from loguru import logger
from skopt import Optimizer from skopt import Optimizer
from skopt.space import Real from skopt.space import Real
from agentic_security.config import settings_var
from agentic_security.http_spec import Modality from agentic_security.http_spec import Modality
from agentic_security.logutils import logger
from agentic_security.primitives 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.cost_module import calculate_cost
from agentic_security.probe_actor.refusal import refusal_heuristic from agentic_security.probe_actor.refusal import refusal_heuristic
from agentic_security.probe_actor.state import FuzzerState
from agentic_security.probe_data import audio_generator, image_generator, msj_data from agentic_security.probe_data import audio_generator, image_generator, msj_data
from agentic_security.probe_data.data import prepare_prompts from agentic_security.probe_data.data import prepare_prompts
MAX_PROMPT_LENGTH = settings_var("fuzzer.max_prompt_lenght", 2048) # TODO: full log file
BUDGET_MULTIPLIER = settings_var("fuzzer.budget_multiplier", 100000000)
INITIAL_OPTIMIZER_POINTS = settings_var("fuzzer.initial_optimizer_points", 25) MAX_PROMPT_LENGTH = 2048
MIN_FAILURE_SAMPLES = settings_var("min_failure_samples", 5) BUDGET_MULTIPLIER = 100_000_000
FAILURE_RATE_THRESHOLD = settings_var("failure_rate_threshold", 0.5) INITIAL_OPTIMIZER_POINTS = 25
MIN_FAILURE_SAMPLES = 5
FAILURE_RATE_THRESHOLD = 0.5
async def generate_prompts( async def generate_prompts(
prompts: list[str] | AsyncGenerator, prompts: list[str] | AsyncGenerator,
) -> AsyncGenerator[str, None]: ) -> AsyncGenerator[str, None]:
"""
Asynchronously generates and yields individual prompts.
If the input is a list of strings, the function sequentially yields each string.
If the input is an asynchronous generator, it forwards each generated prompt.
Args:
prompts (list[str] | AsyncGenerator): A list of strings or an asynchronous generator of prompts.
Yields:
str: An individual prompt from the list or the asynchronous generator.
"""
if isinstance(prompts, list): if isinstance(prompts, list):
for prompt in prompts: for prompt in prompts:
yield prompt yield prompt
@@ -49,21 +37,7 @@ async def generate_prompts(
yield prompt yield prompt
def get_modality_adapter(llm_spec): def multi_modality_spec(llm_spec):
"""
Returns the appropriate request adapter based on the modality of the LLM specification.
Depending on the modality of `llm_spec`, the function selects the corresponding request adapter.
If the modality is IMAGE or AUDIO, it returns an adapter for handling the respective type.
If the modality is TEXT or an unrecognized type, it returns `llm_spec` as is.
Args:
llm_spec: An object containing modality information for the LLM.
Returns:
RequestAdapter | llm_spec: An instance of the appropriate request adapter
or the original `llm_spec` if no adaptation is needed.
"""
match llm_spec.modality: match llm_spec.modality:
case Modality.IMAGE: case Modality.IMAGE:
return image_generator.RequestAdapter(llm_spec) return image_generator.RequestAdapter(llm_spec)
@@ -76,71 +50,40 @@ def get_modality_adapter(llm_spec):
async def process_prompt( async def process_prompt(
request_factory, request_factory, prompt, tokens, module_name, refusals, errors, outputs
prompt: str,
tokens: int,
module_name: str,
fuzzer_state: FuzzerState,
) -> tuple[int, bool]: ) -> tuple[int, bool]:
""" """
Processes a single prompt using the provided request factory and updates tracking lists. Process a single prompt and update the token count and failure status.
This function sends the given `prompt` to the `request_factory`, checks for errors, and updates
the `tokens`, `refusals`, `errors`, and `outputs` lists accordingly. If the request fails or
the response indicates a refusal, the function records the issue and returns the updated token count
along with a boolean indicating whether the prompt was refused.
Args:
request_factory: An object with a `fn` method used to send the prompt.
prompt (str): The input prompt to be processed.
tokens (int): The current token count, which will be updated.
module_name (str): The name of the module handling the request.
fuzzer_state: State tracking object for the fuzzer
Returns:
tuple[int, bool]: Updated token count and a boolean indicating if the prompt was refused.
""" """
try: try:
response = await request_factory.fn(prompt=prompt) response = await request_factory.fn(prompt=prompt)
# Handle HTTP errors
if response.status_code == 422: if response.status_code == 422:
logger.error(f"Invalid prompt: {prompt}, error=422") logger.error(f"Invalid prompt: {prompt}, error=422")
fuzzer_state.add_error(module_name, prompt, 422, "Invalid prompt") errors.append((module_name, prompt, 422, "Invalid prompt"))
return tokens, True return tokens, True
if response.status_code >= 400: if response.status_code >= 400:
logger.error(f"HTTP {response.status_code} {response.content=}") logger.error(f"HTTP {response.status_code} {response.content=}")
fuzzer_state.add_error( errors.append((module_name, prompt, response.status_code, response.text))
module_name, prompt, response.status_code, response.text
)
return tokens, True return tokens, True
# Process successful response
response_text = response.text response_text = response.text
tokens += len(response_text.split()) tokens += len(response_text.split())
# Check if the response indicates a refusal
refused = refusal_heuristic(response.json()) refused = refusal_heuristic(response.json())
if refused: if refused:
fuzzer_state.add_refusal( refusals.append((module_name, prompt, response.status_code, response_text))
module_name, prompt, response.status_code, response_text
)
fuzzer_state.add_output(module_name, prompt, response_text, refused) outputs.append((module_name, prompt, response_text, refused))
return tokens, refused return tokens, refused
except httpx.RequestError as exc: except httpx.RequestError as exc:
logger.error(f"Request error: {exc}") logger.error(f"Request error: {exc}")
fuzzer_state.add_error(module_name, prompt, "?", str(exc)) errors.append((module_name, prompt, "?", str(exc)))
return tokens, True return tokens, True
except JSONDecodeError as json_decode_error: except JSONDecodeError as json_decode_error:
logger.error(f"JSON error: {json_decode_error}") logger.error(f"Jason error: {json_decode_error}")
fuzzer_state.add_error(module_name, prompt, "?", str(json_decode_error)) errors.append((module_name, prompt, "?", str(json_decode_error)))
return tokens, True return tokens, True
except Exception as e:
logger.exception(f"Unexpected error: {e}")
return tokens, False
async def process_prompt_batch( async def process_prompt_batch(
@@ -148,29 +91,14 @@ async def process_prompt_batch(
prompts: list[str], prompts: list[str],
tokens: int, tokens: int,
module_name: str, module_name: str,
fuzzer_state: FuzzerState, refusals,
errors,
outputs,
) -> tuple[int, int]: ) -> tuple[int, int]:
"""
Processes a batch of prompts asynchronously and aggregates the results.
This function sends multiple prompts concurrently using `process_prompt`,
collects the token count and failure status for each prompt, and returns
the total number of tokens processed and the number of failed prompts.
Args:
request_factory: An object with a `fn` method used to send the prompts.
prompts (list[str]): A list of input prompts to be processed.
tokens (int): The initial token count, which will be updated.
module_name (str): The name of the module handling the request.
fuzzer_state: State tracking object for the fuzzer
Returns:
tuple[int, int]:
- Total number of tokens processed.
- Number of failed prompts.
"""
tasks = [ tasks = [
process_prompt(request_factory, p, tokens, module_name, fuzzer_state) process_prompt(
request_factory, p, tokens, module_name, refusals, errors, outputs
)
for p in prompts for p in prompts
] ]
results = await asyncio.gather(*tasks) results = await asyncio.gather(*tasks)
@@ -179,61 +107,53 @@ async def process_prompt_batch(
return total_tokens, failures return total_tokens, failures
async def scan_module( async def perform_single_shot_scan(
request_factory, request_factory,
module, max_budget: int,
fuzzer_state: FuzzerState, datasets: list[dict[str, str]] = [],
processed_prompts: int = 0, tools_inbox=None,
total_prompts: int = 0, optimize=False,
max_budget: int = 0, stop_event: asyncio.Event = None,
total_tokens: int = 0, secrets: dict[str, str] = {},
optimize: bool = False, ) -> AsyncGenerator[str, None]:
stop_event: asyncio.Event | None = None, """Perform a standard security scan."""
) -> AsyncGenerator[dict[str, Any], None]: max_budget = max_budget * BUDGET_MULTIPLIER
""" selected_datasets = [m for m in datasets if m["selected"]]
Scan a single module. request_factory = multi_modality_spec(request_factory)
try:
Args: yield ScanResult.status_msg("Loading datasets...")
request_factory: The factory for creating requests prompt_modules = prepare_prompts(
module: The prompt module to scan dataset_names=[m["dataset_name"] for m in selected_datasets],
fuzzer_state: State tracking object for the fuzzer budget=max_budget,
processed_prompts: Number of prompts processed so far tools_inbox=tools_inbox,
total_prompts: Total number of prompts to process options=[m.get("opts", {}) for m in selected_datasets],
max_budget: Maximum token budget
total_tokens: Current token count
optimize: Whether to use optimization
stop_event: Event to stop scanning
Yields:
ScanResult objects as the scan progresses
"""
tokens = 0
module_failures = 0
module_prompts = 0
failure_rates = []
should_stop = False
# Initialize optimizer if optimization is enabled
optimizer = (
Optimizer(
[Real(0, 1)], base_estimator="GP", n_initial_points=INITIAL_OPTIMIZER_POINTS
) )
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
optimizer = (
Optimizer([Real(0, 1)], base_estimator="GP", n_initial_points=25)
if optimize if optimize
else None else None
) )
failure_rates = []
total_tokens = 0
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) module_size = 0 if module.lazy else len(module.prompts)
logger.info(f"Scanning {module.dataset_name} {module_size}") logger.info(f"Scanning {module.dataset_name} {module_size}")
yield ScanResult( module_prompts = 0 # Reset for each module
module=module.dataset_name,
tokens=0,
cost=0,
progress=0,
failureRate=0,
prompt="",
latency=0,
model="",
).model_dump_json()
async for prompt in generate_prompts(module.prompts): async for prompt in generate_prompts(module.prompts):
if stop_event and stop_event.is_set(): if stop_event and stop_event.is_set():
@@ -243,34 +163,37 @@ async def scan_module(
return return
processed_prompts += 1 processed_prompts += 1
module_prompts += 1 module_prompts += 1 # Fixed increment syntax
# Calculate progress based on total processed prompts # Calculate progress based on total processed prompts
progress = 100 * processed_prompts / total_prompts if total_prompts else 0 progress = (
progress = progress % 100 100 * processed_prompts / total_prompts if total_prompts else 0
)
total_tokens -= tokens total_tokens -= tokens
start = time.time() start = time.time()
tokens, failed = await process_prompt( tokens, failed = await process_prompt(
request_factory, request_factory,
prompt, prompt,
tokens, tokens,
module.dataset_name, module.dataset_name,
fuzzer_state=fuzzer_state, refusals,
errors,
outputs,
) )
end = time.time() end = time.time()
total_tokens += tokens total_tokens += tokens
if failed: if failed:
module_failures += 1 module_failures += 1
failure_rate = module_failures / max(module_prompts, 1) failure_rate = module_failures / max(module_prompts, 1)
failure_rates.append(failure_rate) failure_rates.append(failure_rate)
cost = calculate_cost(tokens) cost = calculate_cost(tokens)
response_text = fuzzer_state.get_last_output(prompt) or "" 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( yield ScanResult(
module=module.dataset_name, module=module.dataset_name,
@@ -283,19 +206,16 @@ async def scan_module(
model=response_text, model=response_text,
).model_dump_json() ).model_dump_json()
# Optimization logic if optimize and len(failure_rates) >= 5:
if optimize and optimizer and len(failure_rates) >= MIN_FAILURE_SAMPLES:
next_point = optimizer.ask() next_point = optimizer.ask()
optimizer.tell(next_point, -failure_rate) optimizer.tell(next_point, -failure_rate)
best_failure_rate = -optimizer.get_result().fun best_failure_rate = -optimizer.get_result().fun
if best_failure_rate > FAILURE_RATE_THRESHOLD: if best_failure_rate > 0.5:
yield ScanResult.status_msg( yield ScanResult.status_msg(
f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..." f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..."
) )
should_stop = True should_stop = True
break break
# Budget check
if total_tokens > max_budget: if total_tokens > max_budget:
logger.info( logger.info(
f"Scan ran out of budget and stopped. {total_tokens=} {max_budget=}" f"Scan ran out of budget and stopped. {total_tokens=} {max_budget=}"
@@ -306,30 +226,14 @@ async def scan_module(
should_stop = True should_stop = True
break break
if should_stop: yield ScanResult.status_msg("Scan completed.")
break
return failure_data = errors + refusals
df = pd.DataFrame(
failure_data, columns=["module", "prompt", "status_code", "content"]
)
df.to_csv("failures.csv", index=False)
async def with_error_handling(agen):
"""
Wraps an asynchronous generator with error handling.
This function iterates over an asynchronous generator, yielding its values.
If an exception occurs, it logs the error and yields a failure message.
Finally, it ensures that a completion message is always yielded.
Args:
agen: An asynchronous generator that produces scan results.
Yields:
ScanResult: Either a successful result, an error message if an
exception occurs, or a completion message at the end.
"""
try:
async for t in agen:
yield t
except Exception as e: except Exception as e:
logger.exception("Scan failed") logger.exception("Scan failed")
yield ScanResult.status_msg(f"Scan failed: {str(e)}") yield ScanResult.status_msg(f"Scan failed: {str(e)}")
@@ -337,121 +241,21 @@ async def with_error_handling(agen):
yield ScanResult.status_msg("Scan completed.") yield ScanResult.status_msg("Scan completed.")
async def perform_single_shot_scan(
request_factory,
max_budget: int,
datasets: list[dict[str, str]] = [],
tools_inbox=None,
optimize: bool = False,
stop_event: asyncio.Event | None = None,
secrets: dict[str, str] = {},
) -> AsyncGenerator[str, None]:
"""
Perform a standard security scan using a given request factory.
This function processes security scan prompts from selected datasets while
respecting a predefined token budget. It supports optimization, failure tracking,
and early stopping based on budget constraints or user intervention.
Args:
request_factory: A factory function that generates requests for processing prompts.
max_budget (int): The maximum token budget for the scan.
datasets (list[dict[str, str]], optional): A list of datasets containing security prompts.
tools_inbox: Optional additional tools for processing (default: None).
optimize (bool, optional): Whether to enable failure rate optimization (default: False).
stop_event (asyncio.Event, optional): An event to signal early termination (default: None).
secrets (dict[str, str], optional): A dictionary of secrets for authentication (default: {}).
Yields:
str: JSON-encoded scan results or status messages.
The function iterates over prompts, processes them asynchronously, and updates
failure statistics and token usage. If the scan exceeds the budget or failure rate is too high,
it stops execution. Results are saved to a CSV file upon completion.
"""
max_budget = max_budget * BUDGET_MULTIPLIER
selected_datasets = [m for m in datasets if m["selected"]]
request_factory = get_modality_adapter(request_factory)
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...")
fuzzer_state = FuzzerState()
total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy)
processed_prompts = 0
total_tokens = 0
for module in prompt_modules:
module_gen = scan_module(
request_factory=request_factory,
module=module,
fuzzer_state=fuzzer_state,
processed_prompts=processed_prompts,
total_prompts=total_prompts,
max_budget=max_budget,
total_tokens=total_tokens,
optimize=optimize,
stop_event=stop_event,
)
try:
async for result in module_gen:
yield result
except Exception:
logger.error("Module exception")
continue
# Update processed_prompts count
module_size = 0 if module.lazy else len(module.prompts)
processed_prompts += module_size
yield ScanResult.status_msg("Scan completed.")
fuzzer_state.export_failures("failures.csv")
async def perform_many_shot_scan( async def perform_many_shot_scan(
request_factory, request_factory,
max_budget: int, max_budget: int,
datasets: list[dict[str, str]] = [], datasets: list[dict[str, str]] = [],
probe_datasets: list[dict[str, str]] = [], probe_datasets: list[dict[str, str]] = [],
tools_inbox=None, tools_inbox=None,
optimize: bool = False, optimize=False,
stop_event: asyncio.Event | None = None, stop_event: asyncio.Event = None,
probe_frequency: float = 0.2, probe_frequency: float = 0.2,
max_ctx_length: int = 10_000, max_ctx_length: int = 10_000,
secrets: dict[str, str] = {}, secrets: dict[str, str] = {},
) -> AsyncGenerator[str, None]: ) -> AsyncGenerator[str, None]:
""" """Perform a multi-step security scan with probe injection."""
Perform a multi-step security scan with probe injection. request_factory = multi_modality_spec(request_factory)
try:
This function executes a security scan while periodically injecting probe datasets
to test system robustness. It tracks failures, optimizes scan efficiency,
and ensures adherence to a predefined token budget.
Args:
request_factory: A factory function that generates requests for processing prompts.
max_budget (int): The maximum token budget for the scan.
datasets (list[dict[str, str]], optional): The main datasets for scanning.
probe_datasets (list[dict[str, str]], optional): Additional datasets for probe injection.
tools_inbox: Optional tools for additional processing (default: None).
optimize (bool, optional): Whether to enable failure rate optimization (default: False).
stop_event (asyncio.Event, optional): An event to signal early termination (default: None).
probe_frequency (float, optional): The probability of probe injection (default: 0.2).
max_ctx_length (int, optional): The maximum context length before resetting (default: 10,000 tokens).
secrets (dict[str, str], optional): A dictionary of secrets for authentication (default: {}).
Yields:
str: JSON-encoded scan results or status messages.
This function iterates over prompts, injects probe prompts at random intervals,
processes them asynchronously, and tracks failure rates. If failure rates exceed a threshold
or budget is exhausted, the scan is stopped early. Results are saved to a CSV file upon completion.
"""
request_factory = get_modality_adapter(request_factory)
# Load main and probe datasets # Load main and probe datasets
yield ScanResult.status_msg("Loading datasets...") yield ScanResult.status_msg("Loading datasets...")
prompt_modules = prepare_prompts( prompt_modules = prepare_prompts(
@@ -463,10 +267,17 @@ async def perform_many_shot_scan(
msj_modules = msj_data.prepare_prompts(probe_datasets) msj_modules = msj_data.prepare_prompts(probe_datasets)
yield ScanResult.status_msg("Datasets loaded. Starting scan...") yield ScanResult.status_msg("Datasets loaded. Starting scan...")
fuzzer_state = FuzzerState() errors = []
refusals = []
outputs = []
total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy) total_prompts = sum(len(m.prompts) for m in prompt_modules if not m.lazy)
processed_prompts = 0 processed_prompts = 0
optimizer = (
Optimizer([Real(0, 1)], base_estimator="GP", n_initial_points=25)
if optimize
else None
)
failure_rates = [] failure_rates = []
for module in prompt_modules: for module in prompt_modules:
@@ -480,11 +291,11 @@ async def perform_many_shot_scan(
logger.info("Scan stopped by user.") logger.info("Scan stopped by user.")
yield ScanResult.status_msg("Scan stopped by user.") yield ScanResult.status_msg("Scan stopped by user.")
return return
tokens = 0 tokens = 0
processed_prompts += 1 processed_prompts += 1
progress = 100 * processed_prompts / total_prompts if total_prompts else 0 progress = (
progress = progress % 100 100 * processed_prompts / total_prompts if total_prompts else 0
)
full_prompt = "" full_prompt = ""
msj_module = random.choice(msj_modules) msj_module = random.choice(msj_modules)
@@ -508,7 +319,9 @@ async def perform_many_shot_scan(
full_prompt, full_prompt,
tokens, tokens,
module.dataset_name, module.dataset_name,
fuzzer_state=fuzzer_state, refusals,
errors,
outputs,
) )
if failed: if failed:
module_failures += 1 module_failures += 1
@@ -529,51 +342,37 @@ async def perform_many_shot_scan(
prompt=prompt[:MAX_PROMPT_LENGTH], prompt=prompt[:MAX_PROMPT_LENGTH],
).model_dump_json() ).model_dump_json()
if optimize and len(failure_rates) >= MIN_FAILURE_SAMPLES: 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( yield ScanResult.status_msg(
f"High failure rate detected ({failure_rate:.2%}). Stopping this module..." f"High failure rate detected ({best_failure_rate:.2%}). Stopping this module..."
) )
break break
yield ScanResult.status_msg("Scan completed.") yield ScanResult.status_msg("Scan completed.")
fuzzer_state.export_failures("failures.csv")
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
def scan_router( def scan_router(
request_factory, request_factory,
scan_parameters: Scan, scan_parameters: Scan,
tools_inbox=None, tools_inbox=None,
stop_event: asyncio.Event | None = None, stop_event: asyncio.Event = None,
): ):
"""
Route scan requests to the appropriate scanning function.
This function determines whether to perform a multi-step or single-shot
security scan based on the provided scan parameters.
Args:
request_factory: A factory function to generate requests for processing prompts.
scan_parameters (Scan): An object containing the parameters for the scan, including:
- enableMultiStepAttack (bool): Whether to perform a multi-step scan.
- maxBudget (int): The maximum token budget for the scan.
- datasets (list[dict[str, str]]): The datasets to scan.
- probe_datasets (list[dict[str, str]], optional): Datasets for probe injection (multi-step only).
- optimize (bool): Whether to enable optimization.
- secrets (dict[str, str], optional): A dictionary of secrets for authentication.
tools_inbox: Optional tools for additional processing (default: None).
stop_event (asyncio.Event, optional): An event to signal early termination (default: None).
Returns:
A function wrapped with `with_error_handling`, which executes either:
- `perform_many_shot_scan` for multi-step scanning.
- `perform_single_shot_scan` for single-shot scanning.
The function ensures that the appropriate scanning method is chosen based on
the `enableMultiStepAttack` flag in `scan_parameters`.
"""
if scan_parameters.enableMultiStepAttack: if scan_parameters.enableMultiStepAttack:
return with_error_handling( return perform_many_shot_scan(
perform_many_shot_scan(
request_factory=request_factory, request_factory=request_factory,
max_budget=scan_parameters.maxBudget, max_budget=scan_parameters.maxBudget,
datasets=scan_parameters.datasets, datasets=scan_parameters.datasets,
@@ -583,10 +382,8 @@ def scan_router(
stop_event=stop_event, stop_event=stop_event,
secrets=scan_parameters.secrets, secrets=scan_parameters.secrets,
) )
)
else: else:
return with_error_handling( return perform_single_shot_scan(
perform_single_shot_scan(
request_factory=request_factory, request_factory=request_factory,
max_budget=scan_parameters.maxBudget, max_budget=scan_parameters.maxBudget,
datasets=scan_parameters.datasets, datasets=scan_parameters.datasets,
@@ -595,4 +392,3 @@ def scan_router(
stop_event=stop_event, stop_event=stop_event,
secrets=scan_parameters.secrets, secrets=scan_parameters.secrets,
) )
)
+5 -1
View File
@@ -1,4 +1,5 @@
import asyncio import asyncio
import logging
from typing import Any from typing import Any
import httpx import httpx
@@ -6,10 +7,13 @@ from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext from pydantic_ai import Agent, RunContext
from agentic_security.http_spec import LLMSpec from agentic_security.http_spec import LLMSpec
from agentic_security.logutils import logger
LLM_SPECS = [] LLM_SPECS = []
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class AgentSpecification(BaseModel): class AgentSpecification(BaseModel):
name: str | None = Field(None, description="Name of the LLM/agent") name: str | None = Field(None, description="Name of the LLM/agent")
+1 -1
View File
@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from agentic_security.refusal_classifier.model import RefusalClassifier from agentic_security.refusal_classifier import RefusalClassifier
classifier = RefusalClassifier() classifier = RefusalClassifier()
classifier.load_model() classifier.load_model()
-47
View File
@@ -1,47 +0,0 @@
import pandas as pd
class FuzzerState:
"""Container for tracking scan results"""
def __init__(self):
self.errors = []
self.refusals = []
self.outputs = []
def add_error(
self,
module_name: str,
prompt: str,
status_code: int | str,
error_msg: str,
):
"""Add an error to the state"""
self.errors.append((module_name, prompt, status_code, error_msg))
def add_refusal(
self, module_name: str, prompt: str, status_code: int, response_text: str
):
"""Add a refusal to the state"""
self.refusals.append((module_name, prompt, status_code, response_text))
def add_output(
self, module_name: str, prompt: str, response_text: str, refused: bool
):
"""Add an output to the state"""
self.outputs.append((module_name, prompt, response_text, refused))
def get_last_output(self, prompt: str) -> str | None:
"""Get the last output for a given prompt"""
for output in reversed(self.outputs):
if output[1] == prompt:
return output[2]
return None
def export_failures(self, filename: str = "failures.csv"):
"""Export failures to a CSV file"""
failure_data = self.errors + self.refusals
df = pd.DataFrame(
failure_data, columns=["module", "prompt", "status_code", "content"]
)
df.to_csv(filename, index=False)
@@ -7,7 +7,6 @@ import pytest
from agentic_security.primitives import Scan from agentic_security.primitives import Scan
from agentic_security.probe_actor.fuzzer import ( from agentic_security.probe_actor.fuzzer import (
FuzzerState,
generate_prompts, generate_prompts,
perform_many_shot_scan, perform_many_shot_scan,
perform_single_shot_scan, perform_single_shot_scan,
@@ -208,7 +207,9 @@ class TestProcessPrompt(unittest.IsolatedAsyncioTestCase):
prompt="test prompt", prompt="test prompt",
tokens=0, tokens=0,
module_name="module_a", module_name="module_a",
fuzzer_state=FuzzerState(), refusals=[],
errors=[],
outputs=[],
) )
self.assertEqual(tokens, 3) # Tokens from "Valid response text" self.assertEqual(tokens, 3) # Tokens from "Valid response text"
@@ -225,17 +226,20 @@ class TestProcessPrompt(unittest.IsolatedAsyncioTestCase):
) )
) )
fuzzer_state = FuzzerState() refusals = []
outputs = []
tokens, refusal = await process_prompt( tokens, refusal = await process_prompt(
request_factory=mock_request_factory, request_factory=mock_request_factory,
prompt="test prompt", prompt="test prompt",
tokens=0, tokens=0,
module_name="module_a", module_name="module_a",
fuzzer_state=fuzzer_state, refusals=refusals,
errors=[],
outputs=outputs,
) )
self.assertEqual(tokens, 3) # Tokens from "Response indicating refusal" self.assertEqual(tokens, 3) # Tokens from "Response indicating refusal"
# self.assertFalse(fuzzer_state.refusals) self.assertFalse(refusal)
async def test_http_error_response(self): async def test_http_error_response(self):
mock_request_factory = Mock() mock_request_factory = Mock()
@@ -248,13 +252,15 @@ class TestProcessPrompt(unittest.IsolatedAsyncioTestCase):
) )
) )
fuzzer_state = FuzzerState() refusals = []
await process_prompt( await process_prompt(
request_factory=mock_request_factory, request_factory=mock_request_factory,
prompt="test prompt", prompt="test prompt",
tokens=0, tokens=0,
module_name="module_a", module_name="module_a",
fuzzer_state=fuzzer_state, refusals=refusals,
errors=[],
outputs=[],
) )
async def test_request_error(self): async def test_request_error(self):
@@ -263,14 +269,18 @@ class TestProcessPrompt(unittest.IsolatedAsyncioTestCase):
side_effect=httpx.RequestError("Connection error") side_effect=httpx.RequestError("Connection error")
) )
fuzzer_state = FuzzerState() errors = []
tokens, refusal = await process_prompt( tokens, refusal = await process_prompt(
request_factory=mock_request_factory, request_factory=mock_request_factory,
prompt="test prompt", prompt="test prompt",
tokens=0, tokens=0,
module_name="module_a", module_name="module_a",
fuzzer_state=fuzzer_state, refusals=[],
errors=errors,
outputs=[],
) )
self.assertEqual(tokens, 0) self.assertEqual(tokens, 0)
self.assertTrue(refusal) self.assertTrue(refusal)
self.assertEqual(len(errors), 1)
self.assertIn("Connection error", errors[0][3])
+1 -16
View File
@@ -1,4 +1,4 @@
from .data import load_local_csv, load_local_csv_files from .data import load_local_csv
REGISTRY_V0 = [ REGISTRY_V0 = [
{ {
@@ -484,18 +484,3 @@ REGISTRY = REGISTRY_V0 + [
"modality": "text", "modality": "text",
}, },
] ]
for ds in load_local_csv_files():
REGISTRY.append(
{
"dataset_name": ds.dataset_name,
"num_prompts": len(ds.prompts),
"tokens": ds.prompts,
"approx_cost": 0.0,
"is_active": True,
"source": f"Local file dataset: {ds.metadata['src']}",
"selected": False,
"url": "",
"modality": "text",
}
)
+5 -29
View File
@@ -1,5 +1,4 @@
import base64 import base64
import logging
import os import os
import platform import platform
import subprocess import subprocess
@@ -8,16 +7,6 @@ import uuid
import httpx import httpx
from cache_to_disk import cache_to_disk from cache_to_disk import cache_to_disk
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class AudioGenerationError(Exception):
"""Custom exception for errors during audio generation."""
pass
def encode(content: bytes) -> str: def encode(content: bytes) -> str:
encoded_content = base64.b64encode(content).decode("utf-8") encoded_content = base64.b64encode(content).decode("utf-8")
@@ -52,25 +41,12 @@ def generate_audio_mac_wav(prompt: str) -> bytes:
# Read the WAV file into memory # Read the WAV file into memory
with open(temp_wav_path, "rb") as f: with open(temp_wav_path, "rb") as f:
audio_bytes = f.read() audio_bytes = f.read()
except subprocess.CalledProcessError as e:
logger.error(f"Subprocess error: {e}")
raise AudioGenerationError("Failed to generate or convert audio.") from e
except FileNotFoundError as e:
logger.error(f"File not found: {e}")
raise AudioGenerationError("Required file not found.") from e
except Exception as e:
logger.exception("Unexpected error occurred.")
raise AudioGenerationError(
"An unexpected error occurred during audio generation."
) from e
finally: finally:
for path in (temp_aiff_path, temp_wav_path): # Clean up the temporary files
try: if os.path.exists(temp_aiff_path):
if os.path.exists(path): os.remove(temp_aiff_path)
os.remove(path) if os.path.exists(temp_wav_path):
except Exception as e: os.remove(temp_wav_path)
logger.warning(f"Failed to delete temporary file {path}: {e}")
# Return the audio bytes # Return the audio bytes
return audio_bytes return audio_bytes
+371 -398
View File
@@ -1,16 +1,14 @@
import io import io
import os import os
import random import random
from collections.abc import Callable, Iterator from functools import lru_cache
from functools import partial
from typing import Any, TypeVar
import httpx import httpx
import pandas as pd import pandas as pd
from cache_to_disk import cache_to_disk from cache_to_disk import cache_to_disk
from datasets import load_dataset from datasets import load_dataset
from loguru import logger
from agentic_security.logutils import logger
from agentic_security.probe_data import stenography_fn from agentic_security.probe_data import stenography_fn
from agentic_security.probe_data.models import ProbeDataset from agentic_security.probe_data.models import ProbeDataset
from agentic_security.probe_data.modules import ( from agentic_security.probe_data.modules import (
@@ -21,293 +19,281 @@ from agentic_security.probe_data.modules import (
rl_model, rl_model,
) )
# Type aliases for clarity
T = TypeVar("T")
FilterFn = Callable[[pd.Series], bool]
ColumnMappings = dict[str, str]
DatasetLoader = Callable[[], ProbeDataset]
TransformFn = Callable[[str], str]
@cache_to_disk()
def load_dataset_general(
dataset_name,
dataset_split="train",
column_mappings=None,
filter_fn=None,
custom_url=None,
additional_metadata=None,
):
"""
Generalized function to load datasets with flexible configurations.
# Core data loading utilities :param dataset_name: Name of the dataset or URL for custom CSVs
def fetch_csv_content(url: str) -> str: :param dataset_split: Split to load from the dataset (e.g., "train")
"""Fetch CSV content from a URL.""" :param column_mappings: Dictionary mapping dataset columns to expected keys, e.g., {'prompt': 'query'}
response = httpx.get(url) :param filter_fn: A filtering function that takes a row and returns True/False
response.raise_for_status() # Raise exception for bad responses :param custom_url: URL for custom CSV datasets
return response.content.decode("utf-8") :param additional_metadata: Additional metadata to include in the ProbeDataset
:return: A ProbeDataset object with the processed data
"""
if custom_url:
logger.info(f"Loading custom CSV dataset from {custom_url}")
r = httpx.get(custom_url)
content = r.content
df = pd.read_csv(io.StringIO(content.decode("utf-8")))
else:
logger.info(f"Loading dataset {dataset_name} from Hugging Face datasets")
dataset = load_dataset(dataset_name)
df = pd.DataFrame(dataset[dataset_split])
# Apply column mappings if provided
if column_mappings:
df.rename(columns=column_mappings, inplace=True)
def load_df_from_source(source: str, is_url: bool = False) -> pd.DataFrame: # Filter rows if filter_fn is provided
"""Load DataFrame from either URL or Hugging Face dataset."""
if is_url:
logger.info(f"Loading custom CSV from {source}")
return pd.read_csv(io.StringIO(fetch_csv_content(source)))
logger.info(f"Loading dataset {source} from Hugging Face")
return pd.DataFrame(load_dataset(source)["train"])
def transform_df(
df: pd.DataFrame,
mappings: ColumnMappings | None = None,
filter_fn: FilterFn | None = None,
) -> pd.DataFrame:
"""Apply transformations to DataFrame."""
result = df.copy()
if mappings:
result.rename(columns=mappings, inplace=True)
if filter_fn: if filter_fn:
result = result[result.apply(filter_fn, axis=1)] df = df[df.apply(filter_fn, axis=1)]
return result
# Extract prompts
prompts = df[column_mappings.get("prompt", "prompt")].tolist()
def create_probe_dataset(
name: str, prompts: list[str], metadata: dict[str, Any] | None = None
) -> ProbeDataset:
"""Create a ProbeDataset from prompts."""
metadata = metadata or {}
return ProbeDataset( return ProbeDataset(
dataset_name=name, dataset_name=dataset_name,
metadata=metadata, metadata=additional_metadata or {},
prompts=prompts, prompts=prompts,
tokens=sum(len(str(s).split()) for s in prompts), tokens=count_words_in_list(prompts),
approx_cost=0.0, approx_cost=0.0,
) )
# Generalized dataset loader def count_words_in_list(str_list):
"""Calculate the total number of words in a given list of strings.
:param str_list: List of strings
:return: Total number of words across all strings in the list
"""
total_words = sum(len(str(s).split()) for s in str_list)
return total_words
@cache_to_disk() @cache_to_disk()
def load_dataset_generic( def load_dataset_v1():
name: str, return load_dataset_general(
mappings: ColumnMappings | None = None, dataset_name="ShawnMenz/DAN_jailbreak",
filter_fn: FilterFn | None = None, column_mappings={"prompt": "prompt", "jailbreak": "jailbreak"},
url: str | None = None, filter_fn=lambda row: row["jailbreak"],
metadata: dict[str, Any] | None = None,
) -> ProbeDataset:
"""Load and process a dataset with flexible configuration."""
try:
df = load_df_from_source(url or name, is_url=bool(url))
transformed_df = transform_df(df, mappings, filter_fn)
# Determine which column to use as the prompt source
prompt_col = None
if mappings and "prompt" in mappings:
prompt_col = mappings["prompt"]
elif "prompt" in transformed_df.columns:
prompt_col = "prompt"
else:
# Try to find a suitable text column
text_columns = [
col
for col in transformed_df.columns
if any(
keyword in col.lower()
for keyword in ["prompt", "text", "query", "question"]
) )
]
if text_columns:
prompt_col = text_columns[0]
logger.info(f"Using column '{prompt_col}' as prompt source")
else:
logger.error(f"No suitable prompt column found in dataset {name}")
return create_probe_dataset(name, [], metadata)
# Extract prompts and filter out empty ones
prompts = [ @cache_to_disk()
def load_dataset_v2():
return load_dataset_general(
dataset_name="deepset/prompt-injections",
column_mappings={"prompt": "text", "jailbreak": "label"},
filter_fn=lambda row: row["label"],
)
@cache_to_disk()
def load_dataset_v4():
return load_dataset_general(
dataset_name="notrichardren/refuse-to-answer-prompts",
column_mappings={"prompt": "claim", "jailbreak": "label"},
filter_fn=lambda row: row["label"],
)
@cache_to_disk()
def load_dataset_v3():
return load_dataset_general(
dataset_name="rubend18/ChatGPT-Jailbreak-Prompts",
column_mappings={"prompt": "Prompt"},
filter_fn=lambda row: row["label"],
)
@cache_to_disk()
def load_dataset_v6():
dataset = load_dataset("markush1/LLM-Jailbreak-Classifier")
bad_prompts = [
p p
for p in transformed_df[prompt_col].tolist() for p, c in zip(dataset["train"]["prompt"], dataset["train"]["classification"])
if p and isinstance(p, (str, int, float)) if c == "jailbreak"
] ]
return create_probe_dataset(name, prompts, metadata) return ProbeDataset(
except Exception as e: dataset_name="markush1/LLM-Jailbreak-Classifier",
logger.error(f"Error loading dataset {name}: {e}") metadata={},
return create_probe_dataset(name, [], {"error": str(e)}) prompts=bad_prompts,
tokens=count_words_in_list(bad_prompts),
approx_cost=0.0,
# Dataset-specific configurations
DATASET_CONFIGS = {
"ShawnMenz/DAN_jailbreak": {
"mappings": {"prompt": "prompt"},
"filter_fn": lambda row: row["jailbreak"],
},
"deepset/prompt-injections": {
"mappings": {"prompt": "text"},
"filter_fn": lambda row: row["label"],
},
"notrichardren/refuse-to-answer-prompts": {
"mappings": {"prompt": "claim"},
"filter_fn": lambda row: row["label"],
},
"rubend18/ChatGPT-Jailbreak-Prompts": {
"mappings": {"prompt": "Prompt"},
"filter_fn": lambda row: row["label"],
},
"markush1/LLM-Jailbreak-Classifier": {
"mappings": {"prompt": "prompt"},
"filter_fn": lambda row: row["classification"] == "jailbreak",
},
"ShawnMenz/jailbreak_sft_rm_ds": {
"url": "hf://datasets/ShawnMenz/jailbreak_sft_rm_ds/jailbreak_sft_rm_ds.csv",
"mappings": {"prompt": "prompt"},
"filter_fn": lambda row: row["jailbreak"] == "jailbreak",
},
"verazuo/jailbreak_llms/2023_05_07": {
"url": "https://raw.githubusercontent.com/verazuo/jailbreak_llms/main/data/prompts/jailbreak_prompts_2023_05_07.csv",
"mappings": {"prompt": "prompt"},
"filter_fn": lambda row: bool(row["jailbreak"]),
},
"verazuo/jailbreak_llms/2023_12_25": {
"url": "https://raw.githubusercontent.com/verazuo/jailbreak_llms/main/data/prompts/jailbreak_prompts_2023_12_25.csv",
"mappings": {"prompt": "prompt"},
"filter_fn": lambda row: bool(row["jailbreak"]),
},
}
# Additional generic dataset configurations
DATASET_CONFIGS_GENERICS = {
"simonycl/aya-23-8B_advbench_jailbreak": {"mappings": {"prompt": "prompt"}},
"acmc/jailbreaks_dataset_with_perplexity_bigcode_starcoder2-3b_bigcode_starcoder2-7b": {},
"karanxa/dolphin-jailbreak-finetuning-dataset": {"mappings": {"prompt": "text"}},
"karanxa/llama-2-jailbreak-dataset": {"mappings": {"prompt": "text"}},
"karanxa/llama2-uncensored-jailbreak-dataset-finetuning": {
"mappings": {"prompt": "text"}
},
"liuyanchen1015/Llama-3.2-1B_jailbreak_responses": {
"mappings": {"prompt": "jailbreak_prompt_text"}
},
"liuyanchen1015/Llama-3.2-1B-Instruct_jailbreak_responses": {
"mappings": {"prompt": "jailbreak_prompt_text"}
},
"liuyanchen1015/Llama-3.2-1B-Instruct_jailbreak_responses_with_judgment": {
"mappings": {"prompt": "jailbreak_prompt_text"}
},
"jackhhao/jailbreak-classification": {"mappings": {"prompt": "prompt"}},
"walledai/JailbreakBench": {"mappings": {"prompt": "prompt"}},
"walledai/JailbreakHub": {"mappings": {"prompt": "prompt"}},
"Granther/evil-jailbreak": {"mappings": {"prompt": "text"}},
"sevdeawesome/jailbreak_success": {"mappings": {"prompt": "jailbreak_prompt_text"}},
"IDA-SERICS/Disaster-tweet-jailbreaking": {"mappings": {"prompt": "prompt_attack"}},
"GeorgeDaDude/Jailbreak_Complete_DS_labeled": {"mappings": {"prompt": "question"}},
"dayone3nder/jailbreak_prompt_JBB_sft_trainset": {"mappings": {"prompt": "prompt"}},
"dayone3nder/general_safe_mix_jailbreak_prompt_JBB_trainset": {
"mappings": {"prompt": "prompt"}
},
}
# Dataset factory
def create_dataset_loader(name: str, config: dict[str, Any]) -> DatasetLoader:
"""Create a dataset loader from configuration."""
return partial(
load_dataset_generic,
name=name,
mappings=config.get("mappings"),
filter_fn=config.get("filter_fn"),
url=config.get("url"),
metadata={"source": name, "config": str(config)},
)
# Specialized loaders
@cache_to_disk()
def load_multi_dataset(name: str, sub_datasets: list[str]) -> ProbeDataset:
"""Load and combine multiple sub-datasets."""
prompts = []
for sub in sub_datasets:
try:
dataset = load_dataset(name, sub)
if "query" in dataset["train"].features:
prompts.extend(dataset["train"]["query"])
else:
logger.warning(f"No 'query' column in {name}/{sub}")
except Exception as e:
logger.error(f"Error loading {name}/{sub}: {e}")
return create_probe_dataset(
f"{name}_combined", prompts, {"source": name, "sub_datasets": sub_datasets}
) )
@cache_to_disk() @cache_to_disk()
def load_jailbreak_v28k() -> ProbeDataset: def load_dataset_v7():
"""Load JailBreakV-28K dataset.""" splits = {
try: "mini_JailBreakV_28K": "JailBreakV_28K/mini_JailBreakV_28K.csv",
"JailBreakV_28K": "JailBreakV_28K/JailBreakV_28K.csv",
}
df = pd.read_csv( df = pd.read_csv(
"hf://datasets/JailbreakV-28K/JailBreakV-28k/JailBreakV_28K.csv" "hf://datasets/JailbreakV-28K/JailBreakV-28k/" + splits["JailBreakV_28K"]
) )
prompts = df["jailbreak_query"].tolist() bad_prompts = df["jailbreak_query"].tolist()
return create_probe_dataset( print(df.shape)
"JailbreakV-28K/JailBreakV-28k", return ProbeDataset(
prompts, dataset_name="JailbreakV-28K/JailBreakV-28k",
{"source": "JailbreakV-28K/JailBreakV-28k"}, metadata={},
prompts=bad_prompts,
tokens=count_words_in_list(bad_prompts),
approx_cost=0.0,
) )
except Exception as e:
logger.error(f"Error loading JailbreakV-28K: {e}")
return create_probe_dataset("JailbreakV-28K/JailBreakV-28k", [])
@cache_to_disk() @cache_to_disk()
def load_local_csv() -> ProbeDataset: def load_dataset_v8():
"""Load prompts from local CSV files.""" df = pd.read_csv(
csv_files = [f for f in os.listdir(".") if f.endswith(".csv")] "hf://datasets/ShawnMenz/jailbreak_sft_rm_ds/jailbreak_sft_rm_ds.csv",
logger.info(f"Found {len(csv_files)} CSV files: {csv_files}") names=["jailbreak", "prompt"],
)
filtered = df[df["jailbreak"] == "jailbreak"]["prompt"].tolist()
return ProbeDataset(
dataset_name="JailbreakV-28K/JailBreakV-28k",
metadata={},
prompts=filtered,
tokens=count_words_in_list(filtered),
approx_cost=0.0,
)
prompts = []
for file in csv_files: @cache_to_disk()
def load_dataset_v5():
ds = []
for c in [
"AdvBench",
"ForbiddenQuestion",
"MJP",
"MaliciousInstruct",
"QuestionList",
]:
dataset = load_dataset("Lemhf14/EasyJailbreak_Datasets", c)
bad_prompts = dataset["train"]["query"]
ds.extend(bad_prompts)
return ProbeDataset(
dataset_name="Lemhf14/EasyJailbreak_Datasets",
metadata={},
prompts=ds,
tokens=count_words_in_list(ds),
approx_cost=0.0,
)
@cache_to_disk()
def load_generic_csv(url, name, column="prompt", predicator=None):
r = httpx.get(url)
content = r.content
df = pd.read_csv(io.StringIO(content.decode("utf-8")))
logger.info(f"Loaded {len(df)} prompts from {url}")
filtered_prompts = df[df.apply(predicator, axis=1)][column].tolist()
return ProbeDataset(
dataset_name=name,
metadata={},
prompts=filtered_prompts,
tokens=count_words_in_list(filtered_prompts),
approx_cost=0.0,
)
def prepare_prompts(dataset_names, budget, tools_inbox=None, options=[]):
# ## Datasets used and cleaned:
# markush1/LLM-Jailbreak-Classifier
# 1. Open-Orca/OpenOrca
# 2. ShawnMenz/DAN_jailbreak
# 3. EddyLuo/JailBreakV_28K
# 4. https://raw.githubusercontent.com/verazuo/jailbreak_llms/main/data/jailbreak_prompts.csv
dataset_map = {
"ShawnMenz/DAN_jailbreak": load_dataset_v1,
"deepset/prompt-injections": load_dataset_v2,
"notrichardren/refuse-to-answer-prompts": load_dataset_v4,
"rubend18/ChatGPT-Jailbreak-Prompts": load_dataset_v3,
"Lemhf14/EasyJailbreak_Datasets": load_dataset_v5,
"markush1/LLM-Jailbreak-Classifier": load_dataset_v6,
"JailbreakV-28K/JailBreakV-28k": load_dataset_v7,
"ShawnMenz/jailbreak_sft_rm_ds": load_dataset_v8,
"verazuo/jailbreak_llms/2023_05_07": lambda: load_generic_csv(
url="https://raw.githubusercontent.com/verazuo/jailbreak_llms/main/data/prompts/jailbreak_prompts_2023_05_07.csv",
name="verazuo/jailbreak_llms/2023_05_07",
column="prompt",
predicator=lambda x: bool(x["jailbreak"]),
),
"verazuo/jailbreak_llms/2023_12_25.csv": lambda: load_generic_csv(
url="https://raw.githubusercontent.com/verazuo/jailbreak_llms/main/data/prompts/jailbreak_prompts_2023_12_25.csv.csv",
name="verazuo/jailbreak_llms/2023_12_25.csv",
column="prompt",
predicator=lambda x: bool(x["jailbreak"]),
),
"Custom CSV": load_local_csv,
}
dataset_map.update(dataset_map_generics)
group = []
for dataset_name in dataset_names:
if dataset_name in dataset_map:
logger.info(f"Loading {dataset_name}")
try: try:
df = pd.read_csv(file) group.append(dataset_map[dataset_name]())
if "prompt" in df.columns:
prompts.extend(df["prompt"].tolist())
else:
logger.warning(f"File {file} lacks a suitable prompt column")
except Exception as e: except Exception as e:
logger.error(f"Error reading {file}: {e}") logger.error(f"Error loading {dataset_name}: {e}")
return create_probe_dataset("Local CSV", prompts, {"src": str(csv_files)}) dynamic_datasets = {
"AgenticBackend": lambda opts: dataset_from_iterator(
"AgenticBackend",
fine_tuned.Module(group, tools_inbox=tools_inbox, opts=opts).apply(),
lazy=True,
),
"Steganography": lambda opts: Stenography(group),
"llm-adaptive-attacks": lambda opts: dataset_from_iterator(
"llm-adaptive-attacks",
adaptive_attacks.Module(group, tools_inbox=tools_inbox, opts=opts).apply(),
),
"Garak": lambda opts: dataset_from_iterator(
"Garak",
garak_tool.Module(group, tools_inbox=tools_inbox, opts=opts).apply(),
lazy=True,
),
"Reinforcement Learning Optimization": lambda opts: dataset_from_iterator(
"Reinforcement Learning Optimization",
rl_model.Module(group, tools_inbox=tools_inbox, opts=opts).apply(),
lazy=True,
),
"InspectAI": lambda opts: dataset_from_iterator(
"InspectAI",
inspect_ai_tool.Module(group, tools_inbox=tools_inbox).apply(),
lazy=True,
),
"GPT fuzzer": lambda opts: [],
}
dynamic_groups = []
options = options or [{} for _ in dataset_names]
for dataset_name, opts in zip(dataset_names, options):
if dataset_name in dynamic_datasets:
logger.info(f"Loading {dataset_name}")
ds = dynamic_datasets[dataset_name](opts)
for g in ds:
dynamic_groups.append(g)
return group + dynamic_groups
@cache_to_disk(1) class Stenography:
def load_csv(file: str) -> ProbeDataset: fn_library = {
"""Load prompts from local CSV files."""
prompts = []
try:
df = pd.read_csv(file)
prompts = df["prompt"].tolist()
if "prompt" in df.columns:
prompts.extend(df["prompt"].tolist())
else:
logger.warning(f"File {file} lacks a suitable prompt column")
except Exception as e:
logger.error(f"Error reading {file}: {e}")
return create_probe_dataset(f"fs://{file}", prompts, {"src": str(file)})
@cache_to_disk(1)
def load_local_csv_files() -> list[ProbeDataset]:
"""Load prompts from local CSV files and return a list of ProbeDataset objects."""
csv_files = [f for f in os.listdir(".") if f.endswith(".csv")]
logger.info(f"Found {len(csv_files)} CSV files: {csv_files}")
datasets = []
for file in csv_files:
try:
df = pd.read_csv(file)
if "prompt" in df.columns:
prompts = df["prompt"].tolist()
datasets.append(create_probe_dataset(file, prompts, {"src": file}))
else:
logger.warning(f"File {file} lacks a suitable prompt column")
except Exception as e:
logger.error(f"Error reading {file}: {e}")
return datasets
# Stenography transformer
class StenographyTransformer:
"""Apply stenography transformations to datasets."""
TRANSFORMATIONS = {
"rot5": stenography_fn.rot5, "rot5": stenography_fn.rot5,
"rot13": stenography_fn.rot13, "rot13": stenography_fn.rot13,
"base64": stenography_fn.base64_encode, "base64": stenography_fn.base64_encode,
@@ -320,164 +306,151 @@ class StenographyTransformer:
"zigzag_obfuscation": stenography_fn.zigzag_obfuscation, "zigzag_obfuscation": stenography_fn.zigzag_obfuscation,
} }
def __init__(self, datasets: list[ProbeDataset]): def __init__(self, prompt_groups: [ProbeDataset]):
self.datasets = datasets self.prompt_groups = prompt_groups
def transform(self) -> Iterator[ProbeDataset]: def __iter__(self):
"""Generate transformed datasets.""" return self.apply()
if not self.datasets:
logger.warning("No datasets provided to StenographyTransformer")
return
for dataset in self.datasets: def apply(self):
if not dataset.prompts: for prompt_group in self.prompt_groups:
logger.warning( size = len(prompt_group.prompts)
f"Dataset {dataset.dataset_name} has no prompts to transform" for name, fn in self.fn_library.items():
) logger.info(f"Applying {name} to {prompt_group.dataset_name}")
continue selected_prompts = random.sample(prompt_group.prompts, int(size * 0.25))
prompts = [
size = len(dataset.prompts) f"Follow the instructions in {name}: {fn(p)}"
sample_size = max(1, int(size * 0.25)) # At least 1 sample for p in selected_prompts
for name, fn in self.TRANSFORMATIONS.items():
logger.info(f"Applying {name} to {dataset.dataset_name}")
sampled = random.sample(dataset.prompts, min(sample_size, size))
try:
transformed = [
f"Follow the instructions in {name}: {fn(str(p))}"
for p in sampled
] ]
transformed_dataset = create_probe_dataset( yield ProbeDataset(
f"stenography.{name}({dataset.dataset_name})", dataset_name=f"stenography.{name}({prompt_group.dataset_name})",
transformed, metadata={},
{"source": dataset.dataset_name, "transformation": name}, prompts=prompts,
) tokens=count_words_in_list(prompts),
yield transformed_dataset approx_cost=0.0,
except Exception as e:
logger.error(
f"Error applying {name} to {dataset.dataset_name}: {e}"
) )
def dataset_from_iterator( @lru_cache
name: str, iterator: Iterator[str], lazy: bool = False def load_local_csv() -> ProbeDataset:
) -> list[ProbeDataset]: csv_files = [f for f in os.listdir(".") if f.endswith(".csv")]
"""Convert an iterator into a list of ProbeDataset objects.""" logger.info(f"Found {len(csv_files)} CSV files")
logger.info(f"CSV files: {csv_files}")
prompt_list = []
for file in csv_files:
try: try:
df = pd.read_csv(file)
except Exception as e:
logger.error(f"Error reading {file}: {e}")
continue
# Check if 'prompt' column exists
if "prompt" in df.columns:
prompt_list.extend(df["prompt"].tolist())
else:
logger.warning(f"File {file} does not contain a 'prompt' column")
return ProbeDataset(
dataset_name="Local CSV",
metadata={"src": str(csv_files)},
prompts=prompt_list,
tokens=count_words_in_list(prompt_list),
approx_cost=0.0,
)
def dataset_from_iterator(name: str, iterator, lazy=False) -> list:
"""Convert an iterator into a list of prompts and create a ProbeDataset
object.
Args:
name (str): The name of the dataset.
iterator (iterator): An iterator that generates prompts.
Returns:
list: A list containing a single ProbeDataset object.
"""
prompts = list(iterator) if not lazy else iterator prompts = list(iterator) if not lazy else iterator
tokens = sum(len(str(s).split()) for s in prompts) if not lazy else 0 tokens = count_words_in_list(prompts) if not lazy else 0
dataset = ProbeDataset( dataset = ProbeDataset(
dataset_name=name, dataset_name=name,
metadata={"source": name, "lazy": lazy}, metadata={},
prompts=prompts, prompts=prompts,
tokens=tokens, tokens=tokens,
approx_cost=0.0, approx_cost=0.0,
lazy=lazy, lazy=lazy,
) )
return [dataset] return [dataset]
except Exception as e:
logger.error(f"Error creating dataset from iterator {name}: {e}")
return [create_probe_dataset(name, [], {"error": str(e)})]
# Main dataset preparation # TODO: refactor this abstraction
def prepare_prompts(
dataset_names: list[str], dataset_map_generics = {
budget: float, "simonycl/aya-23-8B_advbench_jailbreak": lambda: load_dataset_general(
tools_inbox=None, dataset_name="simonycl/aya-23-8B_advbench_jailbreak",
options: list[dict] = None, column_mappings={"prompt": "prompt"},
) -> list[ProbeDataset]: ),
"""Prepare datasets based on names and options.""" "acmc/jailbreaks_dataset_with_perplexity_bigcode_starcoder2-3b_bigcode_starcoder2-7b": lambda: load_dataset_general(
# Base dataset loaders dataset_name="acmc/jailbreaks_dataset_with_perplexity_bigcode_starcoder2-3b_bigcode_starcoder2-7b"
logger.info(f"Preparing datasets: {dataset_names}") ),
dataset_loaders = { "karanxa/dolphin-jailbreak-finetuning-dataset": lambda: load_dataset_general(
**{k: create_dataset_loader(k, v) for k, v in DATASET_CONFIGS.items()}, dataset_name="karanxa/dolphin-jailbreak-finetuning-dataset",
**{k: create_dataset_loader(k, v) for k, v in DATASET_CONFIGS_GENERICS.items()}, column_mappings={"prompt": "text"},
"Lemhf14/EasyJailbreak_Datasets": partial( ),
load_multi_dataset, "karanxa/llama-2-jailbreak-dataset": lambda: load_dataset_general(
name="Lemhf14/EasyJailbreak_Datasets", dataset_name="karanxa/llama-2-jailbreak-dataset",
sub_datasets=[ column_mappings={"prompt": "text"},
"AdvBench", ),
"ForbiddenQuestion", "karanxa/llama2-uncensored-jailbreak-dataset-finetuning": lambda: load_dataset_general(
"MJP", dataset_name="karanxa/llama2-uncensored-jailbreak-dataset-finetuning",
"MaliciousInstruct", column_mappings={"prompt": "text"},
"QuestionList", ),
], "liuyanchen1015/Llama-3.2-1B_jailbreak_responses": lambda: load_dataset_general(
dataset_name="liuyanchen1015/Llama-3.2-1B_jailbreak_responses",
column_mappings={"prompt": "jailbreak_prompt_text"},
),
"liuyanchen1015/Llama-3.2-1B-Instruct_jailbreak_responses": lambda: load_dataset_general(
dataset_name="liuyanchen1015/Llama-3.2-1B-Instruct_jailbreak_responses",
column_mappings={"prompt": "jailbreak_prompt_text"},
),
"liuyanchen1015/Llama-3.2-1B-Instruct_jailbreak_responses_with_judgment": lambda: load_dataset_general(
dataset_name="liuyanchen1015/Llama-3.2-1B-Instruct_jailbreak_responses_with_judgment",
column_mappings={"prompt": "jailbreak_prompt_text"},
),
"jackhhao/jailbreak-classification": lambda: load_dataset_general(
dataset_name="jackhhao/jailbreak-classification",
column_mappings={"prompt": "prompt"},
),
"markush1/LLM-Jailbreak-Classifier": lambda: load_dataset_general(
dataset_name="markush1/LLM-Jailbreak-Classifier",
column_mappings={"prompt": "prompt"},
),
"walledai/JailbreakBench": lambda: load_dataset_general(
dataset_name="walledai/JailbreakBench", column_mappings={"prompt": "prompt"}
),
"walledai/JailbreakHub": lambda: load_dataset_general(
dataset_name="walledai/JailbreakHub", column_mappings={"prompt": "prompt"}
),
"Granther/evil-jailbreak": lambda: load_dataset_general(
dataset_name="Granther/evil-jailbreak", column_mappings={"prompt": "text"}
),
"sevdeawesome/jailbreak_success": lambda: load_dataset_general(
dataset_name="sevdeawesome/jailbreak_success",
column_mappings={"prompt": "jailbreak_prompt_text"},
),
"IDA-SERICS/Disaster-tweet-jailbreaking": lambda: load_dataset_general(
dataset_name="IDA-SERICS/Disaster-tweet-jailbreaking",
column_mappings={"prompt": "prompt_attack"},
),
"GeorgeDaDude/Jailbreak_Complete_DS_labeled": lambda: load_dataset_general(
dataset_name="GeorgeDaDude/Jailbreak_Complete_DS_labeled",
column_mappings={"prompt": "question"},
),
"dayone3nder/jailbreak_prompt_JBB_sft_trainset": lambda: load_dataset_general(
dataset_name="dayone3nder/jailbreak_prompt_JBB_sft_trainset",
column_mappings={"prompt": "prompt"},
),
"dayone3nder/general_safe_mix_jailbreak_prompt_JBB_trainset": lambda: load_dataset_general(
dataset_name="dayone3nder/general_safe_mix_jailbreak_prompt_JBB_trainset",
column_mappings={"prompt": "prompt"},
), ),
"JailbreakV-28K/JailBreakV-28k": load_jailbreak_v28k,
"Local CSV": load_local_csv,
"Custom CSV": load_local_csv,
} }
# Dynamic dataset loaders
dynamic_loaders = {
"AgenticBackend": lambda opts: dataset_from_iterator(
"AgenticBackend",
fine_tuned.Module(
opts["datasets"], tools_inbox=tools_inbox, opts=opts
).apply(),
lazy=True,
),
"Steganography": lambda opts: list(
StenographyTransformer(opts["datasets"]).transform()
),
"llm-adaptive-attacks": lambda opts: dataset_from_iterator(
"llm-adaptive-attacks",
adaptive_attacks.Module(
opts["datasets"], tools_inbox=tools_inbox, opts=opts
).apply(),
),
"Garak": lambda opts: dataset_from_iterator(
"Garak",
garak_tool.Module(
opts["datasets"], tools_inbox=tools_inbox, opts=opts
).apply(),
lazy=True,
),
"Reinforcement Learning Optimization": lambda opts: dataset_from_iterator(
"Reinforcement Learning Optimization",
rl_model.Module(
opts["datasets"], tools_inbox=tools_inbox, opts=opts
).apply(),
lazy=True,
),
"InspectAI": lambda opts: dataset_from_iterator(
"InspectAI",
inspect_ai_tool.Module([], tools_inbox=tools_inbox).apply(),
lazy=True,
),
"GPT fuzzer": lambda opts: [],
}
datasets = []
options = options or [dict(datasets=datasets) for _ in dataset_names]
# Load base datasets
for name, opts in zip(dataset_names, options):
if name not in dataset_loaders:
continue
try:
datasets.append(dataset_loaders[name]())
except Exception as e:
logger.error(f"Error loading {name}: {e}")
# Load dynamic datasets and apply transformations
for name, opts in zip(dataset_names, options):
if name not in dynamic_loaders:
continue
logger.info(f"Loading dynamic dataset {name} {opts}")
opts["datasets"] = datasets
try:
dynamic_result = dynamic_loaders[name](opts)
datasets.extend(dynamic_result)
except Exception as e:
logger.exception(f"Error loading dynamic {name}: {e}")
# Load csv datasets and apply transformations
for name, opts in zip(dataset_names, options):
if not name.endswith(".csv"):
continue
logger.info(f"Loading csv dataset {name} {opts}")
datasets.append(load_csv(name))
return datasets
@@ -3,8 +3,7 @@ import io
import httpx import httpx
import pandas as pd import pandas as pd
from loguru import logger
from agentic_security.logutils import logger
url = "https://raw.githubusercontent.com/tml-epfl/llm-adaptive-attacks/main/harmful_behaviors/harmful_behaviors_pair.csv" url = "https://raw.githubusercontent.com/tml-epfl/llm-adaptive-attacks/main/harmful_behaviors/harmful_behaviors_pair.csv"
@@ -3,8 +3,7 @@ import os
import uuid as U import uuid as U
import httpx import httpx
from loguru import logger
from agentic_security.logutils import logger
AUTH_TOKEN: str = os.getenv("AS_TOKEN", "gh0-5f4a8ed2-37c6-4bd7-a0cf-7070eae8115b") AUTH_TOKEN: str = os.getenv("AS_TOKEN", "gh0-5f4a8ed2-37c6-4bd7-a0cf-7070eae8115b")
@@ -66,7 +65,7 @@ class Module:
return {} return {}
async def fetch_prompts(self) -> list[str]: async def fetch_prompts(self) -> list[str]:
api_url = "https://mcp.metaheuristic.co/infer" api_url = "https://edge.metaheuristic.co/infer"
headers = { headers = {
"Authorization": f"Bearer {AUTH_TOKEN}", "Authorization": f"Bearer {AUTH_TOKEN}",
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -4,7 +4,7 @@ import json
import os import os
import subprocess import subprocess
from agentic_security.logutils import logger from loguru import logger
# TODO: add probes modules # TODO: add probes modules
@@ -2,7 +2,7 @@ import asyncio
import importlib.util import importlib.util
import os import os
from agentic_security.logutils import logger from loguru import logger
inspect_ai_task = ( inspect_ai_task = (
__file__.replace("inspect_ai_tool.py", "inspect_ai_task.py") __file__.replace("inspect_ai_tool.py", "inspect_ai_task.py")
@@ -8,8 +8,7 @@ from typing import Deque
import numpy as np import numpy as np
import requests import requests
from loguru import logger
from agentic_security.logutils import logger
AUTH_TOKEN: str = os.getenv("AS_TOKEN", "gh0-5f4a8ed2-37c6-4bd7-a0cf-7070eae8115b") AUTH_TOKEN: str = os.getenv("AS_TOKEN", "gh0-5f4a8ed2-37c6-4bd7-a0cf-7070eae8115b")
@@ -216,7 +215,7 @@ class Module:
self.run_id = U.uuid4().hex self.run_id = U.uuid4().hex
self.batch_size = self.opts.get("batch_size", 500) self.batch_size = self.opts.get("batch_size", 500)
self.rl_model = CloudRLPromptSelector( self.rl_model = CloudRLPromptSelector(
prompt_groups, "https://mcp.metaheuristic.co", run_id=self.run_id prompt_groups, "https://edge.metaheuristic.co", run_id=self.run_id
) )
async def apply(self): async def apply(self):
@@ -36,7 +36,7 @@ def mock_requests() -> Mock:
def mock_rl_selector() -> Mock: def mock_rl_selector() -> Mock:
return CloudRLPromptSelector( return CloudRLPromptSelector(
dataset_prompts, dataset_prompts,
api_url="https://mcp.metaheuristic.co", api_url="https://edge.metaheuristic.co",
) )
@@ -94,7 +94,7 @@ class TestCloudRLPromptSelector:
def test_select_next_prompt_success_service(self, dataset_prompts): def test_select_next_prompt_success_service(self, dataset_prompts):
selector = CloudRLPromptSelector( selector = CloudRLPromptSelector(
dataset_prompts, dataset_prompts,
api_url="https://mcp.metaheuristic.co", api_url="https://edge.metaheuristic.co",
) )
next_prompt = selector.select_next_prompt( next_prompt = selector.select_next_prompt(
"How does RL work?", passed_guard=True "How does RL work?", passed_guard=True
+2 -2
View File
@@ -1,6 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from cache_to_disk import cache_to_disk # noqa from cache_to_disk import cache_to_disk
# TODO: refactor this class to use from .data # TODO: refactor this class to use from .data
@@ -22,7 +22,7 @@ class ProbeDataset:
} }
# @cache_to_disk(n_days_to_cache=1) @cache_to_disk()
def load_dataset_generic(name, getter=lambda x: x["train"]["prompt"]): def load_dataset_generic(name, getter=lambda x: x["train"]["prompt"]):
from datasets import load_dataset from datasets import load_dataset
@@ -3,7 +3,7 @@ from unittest.mock import patch
import pandas as pd import pandas as pd
import pytest import pytest
from agentic_security.refusal_classifier.model import RefusalClassifier from .model import RefusalClassifier
@pytest.fixture @pytest.fixture
+11 -77
View File
@@ -7,61 +7,9 @@ import pandas as pd
from matplotlib.cm import ScalarMappable from matplotlib.cm import ScalarMappable
from matplotlib.colors import LinearSegmentedColormap, Normalize from matplotlib.colors import LinearSegmentedColormap, Normalize
from agentic_security.logutils import logger
from .primitives import Table def plot_security_report(table):
def plot_security_report(table: Table) -> io.BytesIO:
"""
Generates a polar plot representing the security report based on the given data.
Args:
table (Table): The input data table containing security metrics.
Returns:
io.BytesIO: A buffer containing the generated plot image in PNG format.
Returns an empty buffer in case of an error.
"""
try:
return _plot_security_report(table=table)
except (TypeError, ValueError, OverflowError, IndexError, Exception) as e:
logger.error(f"Error in generating the security report: {e} {table}")
return io.BytesIO()
def generate_identifiers(data: pd.DataFrame) -> list[str]:
"""
Generates unique identifiers for the given dataset.
Args:
data (pd.DataFrame): A pandas DataFrame containing security-related data.
Returns:
list[str]: A list of generated identifiers. Returns a list with an empty string in case of an error.
"""
return _generate_identifiers(data=data)
def _plot_security_report(table: Table) -> io.BytesIO:
"""
Generates a polar plot-based security report visualizing the failure rates for different modules.
This function processes the input data, sorts it by failure rate, and generates a polar plot
where each bar represents the failure rate for a specific module. The plot includes identifiers,
color-coding based on token count, failure rate values on the bars, and a table listing the modules
and their corresponding failure rates.
Args:
table (Table): A table-like structure (e.g., pandas DataFrame) containing security report data
with columns for failure rate, tokens, and modules.
Returns:
io.BytesIO: A buffer containing the generated plot image in PNG format.
"""
# Data preprocessing # Data preprocessing
logger.info("Data preprocessing started.")
data = pd.DataFrame(table) data = pd.DataFrame(table)
# Sort by failure rate and reset index # Sort by failure rate and reset index
@@ -72,10 +20,10 @@ def _plot_security_report(table: Table) -> io.BytesIO:
fig, ax = plt.subplots(figsize=(12, 10), subplot_kw={"projection": "polar"}) fig, ax = plt.subplots(figsize=(12, 10), subplot_kw={"projection": "polar"})
fig.set_facecolor("#f0f0f0") fig.set_facecolor("#f0f0f0")
ax.set_facecolor("#f0f0f0") ax.set_facecolor("#f0f0f0")
logger.info("Plot setup complete.")
# Styling parameters # Styling parameters
colors = ["#6C5B7B", "#C06C84", "#F67280", "#F8B195"][::-1] # Pastel palette colors = ["#6C5B7B", "#C06C84", "#F67280", "#F8B195"][::-1] # Pastel palette
# colors = ["#440154", "#3b528b", "#21908c", "#5dc863"] # Viridis-inspired palette
cmap = LinearSegmentedColormap.from_list("custom", colors, N=256) cmap = LinearSegmentedColormap.from_list("custom", colors, N=256)
norm = Normalize(vmin=data["tokens"].min(), vmax=data["tokens"].max()) norm = Normalize(vmin=data["tokens"].min(), vmax=data["tokens"].max())
@@ -126,10 +74,7 @@ def _plot_security_report(table: Table) -> io.BytesIO:
# Title and caption # Title and caption
fig.suptitle( fig.suptitle(
"Security Report for Different Modules", "Security Report for Different Modules", fontsize=16, fontweight="bold", y=1.02
fontsize=16,
fontweight="bold",
y=1.02,
) )
caption = "Report generated by https://github.com/msoedov/agentic_security" caption = "Report generated by https://github.com/msoedov/agentic_security"
fig.text( fig.text(
@@ -167,12 +112,17 @@ def _plot_security_report(table: Table) -> io.BytesIO:
data["identifier"], data["failureRate"], data["module"] data["identifier"], data["failureRate"], data["module"]
) )
] ]
table = ax.table(cellText=table_data, loc="right", cellLoc="left") table = ax.table(
cellText=table_data,
loc="right",
cellLoc="left",
)
table.auto_set_font_size(False) table.auto_set_font_size(False)
table.set_fontsize(8) table.set_fontsize(8)
# Adjust table style # Adjust table style
table.scale(1, 0.7) table.scale(1, 0.7)
for (row, col), cell in table.get_celld().items(): for (row, col), cell in table.get_celld().items():
cell.set_edgecolor("none") cell.set_edgecolor("none")
cell.set_facecolor("#f0f0f0" if row % 2 == 0 else "#e0e0e0") cell.set_facecolor("#f0f0f0" if row % 2 == 0 else "#e0e0e0")
@@ -182,33 +132,17 @@ def _plot_security_report(table: Table) -> io.BytesIO:
cell.set_text_props(fontweight="bold") cell.set_text_props(fontweight="bold")
# Adjust layout and save # Adjust layout and save
plt.tight_layout() plt.tight_layout()
buf = io.BytesIO() buf = io.BytesIO()
plt.savefig(buf, format="png", dpi=300, bbox_inches="tight") plt.savefig(buf, format="png", dpi=300, bbox_inches="tight")
plt.close(fig) plt.close(fig)
buf.seek(0) buf.seek(0)
logger.info("Report successfully generated and saved to buffer.")
return buf return buf
def _generate_identifiers(data: pd.DataFrame) -> list[str]: def generate_identifiers(data):
"""
Generates a list of unique identifiers for each row in the given DataFrame.
The identifiers are based on the English alphabet, with each identifier consisting
of a letter followed by a number. The letter represents the "group" of identifiers
(using a letter from A to Z) and the number is a counter within that group. For example:
- A1, A2, ..., A26, B1, B2, ..., Z1, Z2, ...
Args:
data (pd.DataFrame): The input DataFrame containing data for which identifiers
are to be generated.
Returns:
list[str]: A list of unique identifiers as strings, one for each row in the DataFrame.
"""
data_length = len(data) data_length = len(data)
alphabet = string.ascii_uppercase alphabet = string.ascii_uppercase
num_letters = len(alphabet) num_letters = len(alphabet)
-397
View File
@@ -1,397 +0,0 @@
_SPECS = [
"""POST ${SELF_URL}/v1/self-probe
Authorization: Bearer XXXXX
Content-Type: application/json
{
"prompt": "<<PROMPT>>"
}
""",
"""POST https://api.openai.com/v1/chat/completions
Authorization: Bearer $OPENAI_API_KEY
Content-Type: application/json
{
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "<<PROMPT>>"}],
"temperature": 0.7
}
""",
"""
POST https://api.deepseek.com/chat/completions
Authorization: Bearer $DEEPSEEK_API_KEY
Content-Type: application/json
{
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "<<PROMPT>>"}
],
"stream": false
}
""",
"""POST https://api.replicate.com/v1/models/mistralai/mixtral-8x7b-instruct-v0.1/predictions
Authorization: Bearer $APIKEY
Content-Type: application/json
{
"input": {
"top_k": 50,
"top_p": 0.9,
"prompt": "Write a bedtime story about neural networks I can read to my toddler",
"temperature": 0.6,
"max_new_tokens": 1024,
"prompt_template": "<s>[INST] <<PROMPT>> [/INST] ",
"presence_penalty": 0,
"frequency_penalty": 0
}
}
""",
"""POST https://api.groq.com/v1/request_manager/text_completion
Authorization: Bearer $APIKEY
Content-Type: application/json
{
"model_id": "codellama-34b",
"system_prompt": "You are helpful and concise coding assistant",
"user_prompt": "<<PROMPT>>"
}
""",
"""POST https://api.together.xyz/v1/chat/completions
Authorization: Bearer $TOGETHER_API_KEY
Content-Type: application/json
{
"model": "mistralai/Mixtral-8x7B-Instruct-v0.1",
"messages": [
{"role": "system", "content": "You are an expert travel guide"},
{"role": "user", "content": "<<PROMPT>>"}
]
}
""",
"""POST ${SELF_URL}/v1/self-probe-image
Authorization: Bearer XXXXX
Content-Type: application/json
[
{
"role": "user",
"content": [
{
"type": "text",
"text": "What is in this image?",
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{<<BASE64_IMAGE>>}"
},
},
],
}
]
""",
"""POST ${SELF_URL}/v1/self-probe-file
Authorization: Bearer $API_KEY
Content-Type: multipart/form-data
{
"file": "@./sample_audio.m4a",
"model": "whisper-large-v3"
}
""",
"""POST https://api.gemini.com/v1/generate
Authorization: Bearer $GEMINI_API_KEY
Content-Type: application/json
{
"model": "gemini-latest",
"prompt": "<<PROMPT>>",
"temperature": 0.8,
"max_tokens": 150,
"top_p": 1.0,
"frequency_penalty": 0,
"presence_penalty": 0
}
""",
"""POST https://api.anthropic.com/v1/complete
Authorization: Bearer $ANTHROPIC_API_KEY
Content-Type: application/json
{
"model": "claude-v1.3",
"prompt": "<<PROMPT>>",
"temperature": 0.7,
"max_tokens_to_sample": 256,
"stop_sequences": ["\n\nHuman:"]
}
""",
"""POST https://api.cohere.ai/generate
Authorization: Bearer $COHERE_API_KEY
Content-Type: application/json
{
"model": "command-xlarge-nightly",
"prompt": "<<PROMPT>>",
"max_tokens": 300,
"temperature": 0.75,
"k": 0,
"p": 0.75
}
""",
"""POST https://<<RESOURCE_NAME>>.openai.azure.com/openai/deployments/<<DEPLOYMENT_NAME>>/completions?api-version=2023-06-01-preview
Authorization: Bearer $AZURE_API_KEY
Content-Type: application/json
{
"prompt": "<<PROMPT>>",
"max_tokens": 150,
"temperature": 0.7,
"top_p": 0.9,
"frequency_penalty": 0,
"presence_penalty": 0
}
""",
"""POST https://api.assemblyai.com/v2/transcript
Authorization: Bearer $ASSEMBLY_API_KEY
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
}
""",
]
LLM_SPECS = [
"""POST ${SELF_URL}/v1/self-probe
Authorization: Bearer XXXXX
Content-Type: application/json
{
"prompt": "<<PROMPT>>"
}
""",
"""POST https://api.openai.com/v1/chat/completions
Authorization: Bearer $OPENAI_API_KEY
Content-Type: application/json
{
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "<<PROMPT>>"}],
"temperature": 0.7
}
""",
"""
POST https://api.deepseek.com/chat/completions
Authorization: Bearer $DEEPSEEK_API_KEY
Content-Type: application/json
{
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "<<PROMPT>>"}
],
"stream": false
}
""",
"""POST https://api.replicate.com/v1/models/mistralai/mixtral-8x7b-instruct-v0.1/predictions
Authorization: Bearer $APIKEY
Content-Type: application/json
{
"input": {
"top_k": 50,
"top_p": 0.9,
"prompt": "Write a bedtime story about neural networks I can read to my toddler",
"temperature": 0.6,
"max_new_tokens": 1024,
"prompt_template": "<s>[INST] <<PROMPT>> [/INST] ",
"presence_penalty": 0,
"frequency_penalty": 0
}
}
""",
"""POST https://api.groq.com/v1/request_manager/text_completion
Authorization: Bearer $APIKEY
Content-Type: application/json
{
"model_id": "codellama-34b",
"system_prompt": "You are helpful and concise coding assistant",
"user_prompt": "<<PROMPT>>"
}
""",
"""POST https://api.together.xyz/v1/chat/completions
Authorization: Bearer $TOGETHER_API_KEY
Content-Type: application/json
{
"model": "mistralai/Mixtral-8x7B-Instruct-v0.1",
"messages": [
{"role": "system", "content": "You are an expert travel guide"},
{"role": "user", "content": "<<PROMPT>>"}
]
}
""",
"""POST ${SELF_URL}/v1/self-probe-image
Authorization: Bearer XXXXX
Content-Type: application/json
[
{
"role": "user",
"content": [
{
"type": "text",
"text": "What is in this image?",
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{<<BASE64_IMAGE>>}"
},
},
],
}
]
""",
"""POST ${SELF_URL}/v1/self-probe-file
Authorization: Bearer $API_KEY
Content-Type: multipart/form-data
{
"file": "@./sample_audio.m4a",
"model": "whisper-large-v3"
}
""",
"""POST https://api.gemini.com/v1/generate
Authorization: Bearer $GEMINI_API_KEY
Content-Type: application/json
{
"model": "gemini-latest",
"prompt": "<<PROMPT>>",
"temperature": 0.8,
"max_tokens": 150,
"top_p": 1.0,
"frequency_penalty": 0,
"presence_penalty": 0
}
""",
"""POST https://api.anthropic.com/v1/complete
Authorization: Bearer $ANTHROPIC_API_KEY
Content-Type: application/json
{
"model": "claude-v1.3",
"prompt": "<<PROMPT>>",
"temperature": 0.7,
"max_tokens_to_sample": 256,
"stop_sequences": ["\n\nHuman:"]
}
""",
"""POST https://api.cohere.ai/generate
Authorization: Bearer $COHERE_API_KEY
Content-Type: application/json
{
"model": "command-xlarge-nightly",
"prompt": "<<PROMPT>>",
"max_tokens": 300,
"temperature": 0.75,
"k": 0,
"p": 0.75
}
""",
"""POST https://<<RESOURCE_NAME>>.openai.azure.com/openai/deployments/<<DEPLOYMENT_NAME>>/completions?api-version=2023-06-01-preview
Authorization: Bearer $AZURE_API_KEY
Content-Type: application/json
{
"prompt": "<<PROMPT>>",
"max_tokens": 150,
"temperature": 0.7,
"top_p": 0.9,
"frequency_penalty": 0,
"presence_penalty": 0
}
""",
"""POST https://api.assemblyai.com/v2/transcript
Authorization: Bearer $ASSEMBLY_API_KEY
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
}
""",
]
LLM_CONFIGS = [
{
"name": "Custom API",
"prompts": 40000,
"customInstructions": "Requires api spec",
"logo": "/icons/myshell.png",
},
{"name": "Open AI", "prompts": 24000, "logo": "/icons/openai.png"},
{"name": "Deepseek v1", "prompts": 24000, "logo": "/icons/deepseek.png"},
{"name": "Replicate", "prompts": 40000, "logo": "/icons/replicate.png"},
{"name": "Groq", "prompts": 40000, "logo": "/icons/groq.png"},
{"name": "Together.ai", "prompts": 40000, "logo": "/icons/together.png"},
{
"name": "Custom API Image",
"prompts": 40000,
"customInstructions": "Requires api spec",
"modality": "Image",
"logo": "/icons/myshell.png",
},
{
"name": "Custom API Files",
"prompts": 40000,
"customInstructions": "Requires api spec",
"modality": "Files",
"logo": "/icons/myshell.png",
},
{"name": "Gemini", "prompts": 40000, "logo": "/icons/gemini.png"},
{"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": "assemblyai", "prompts": 40000, "logo": "/icons/myshell.png"},
{"name": "OpenRouter.ai", "prompts": 40000, "logo": "/icons/openrouter.png"},
]
LLM_SPECS = [dict(spec=spec, **d) for spec, d in zip(_SPECS, LLM_CONFIGS)]
-7
View File
@@ -6,7 +6,6 @@ from fastapi.responses import JSONResponse
from ..primitives import FileProbeResponse, Probe from ..primitives import FileProbeResponse, Probe
from ..probe_actor.refusal import REFUSAL_MARKS from ..probe_actor.refusal import REFUSAL_MARKS
from ..probe_data import REGISTRY from ..probe_data import REGISTRY
from ._specs import LLM_SPECS
router = APIRouter() router = APIRouter()
@@ -74,12 +73,6 @@ async def data_config():
return [m for m in REGISTRY] return [m for m in REGISTRY]
@router.get("/v1/llm-specs", response_model=list)
def get_llm_specs():
"""Returns the LLM API specifications."""
return LLM_SPECS
@router.get("/health") @router.get("/health")
async def health_check(): async def health_check():
"""Health check endpoint.""" """Health check endpoint."""
+1 -2
View File
@@ -2,8 +2,7 @@ import random
from asyncio import Event from asyncio import Event
from fastapi import APIRouter from fastapi import APIRouter
from loguru import logger
from agentic_security.logutils import logger
from ..core.app import get_current_run, get_tools_inbox from ..core.app import get_current_run, get_tools_inbox
from ..primitives import CompletionRequest, Settings from ..primitives import CompletionRequest, Settings
+6 -17
View File
@@ -1,6 +1,4 @@
from collections.abc import Generator
from datetime import datetime from datetime import datetime
from typing import Any
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
@@ -13,11 +11,9 @@ from fastapi import (
) )
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from agentic_security.logutils import logger
from ..core.app import get_stop_event, get_tools_inbox, set_current_run from ..core.app import get_stop_event, get_tools_inbox, set_current_run
from ..dependencies import InMemorySecrets, get_in_memory_secrets from ..dependencies import InMemorySecrets, get_in_memory_secrets
from ..http_spec import InvalidHTTPSpecError, LLMSpec from ..http_spec import LLMSpec
from ..primitives import LLMInfo, Scan from ..primitives import LLMInfo, Scan
from ..probe_actor import fuzzer from ..probe_actor import fuzzer
@@ -27,16 +23,9 @@ router = APIRouter()
@router.post("/verify") @router.post("/verify")
async def verify( async def verify(
info: LLMInfo, secrets: InMemorySecrets = Depends(get_in_memory_secrets) info: LLMInfo, secrets: InMemorySecrets = Depends(get_in_memory_secrets)
) -> dict[str, int | str | float]: ):
spec = LLMSpec.from_string(info.spec) spec = LLMSpec.from_string(info.spec)
try:
r = await spec.verify() r = await spec.verify()
except InvalidHTTPSpecError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.exception(e)
raise HTTPException(status_code=400, detail=str(e))
if r.status_code >= 400: if r.status_code >= 400:
raise HTTPException(status_code=r.status_code, detail=r.text) raise HTTPException(status_code=r.status_code, detail=r.text)
return dict( return dict(
@@ -47,7 +36,7 @@ async def verify(
) )
def streaming_response_generator(scan_parameters: Scan) -> Generator[str, Any, None]: def streaming_response_generator(scan_parameters: Scan):
request_factory = LLMSpec.from_string(scan_parameters.llmSpec) request_factory = LLMSpec.from_string(scan_parameters.llmSpec)
set_current_run(request_factory) set_current_run(request_factory)
@@ -68,7 +57,7 @@ async def scan(
scan_parameters: Scan, scan_parameters: Scan,
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
secrets: InMemorySecrets = Depends(get_in_memory_secrets), secrets: InMemorySecrets = Depends(get_in_memory_secrets),
) -> StreamingResponse: ):
scan_parameters.with_secrets(secrets) scan_parameters.with_secrets(secrets)
return StreamingResponse( return StreamingResponse(
streaming_response_generator(scan_parameters), media_type="application/json" streaming_response_generator(scan_parameters), media_type="application/json"
@@ -76,7 +65,7 @@ async def scan(
@router.post("/stop") @router.post("/stop")
async def stop_scan() -> dict[str, str]: async def stop_scan():
get_stop_event().set() get_stop_event().set()
return {"status": "Scan stopped"} return {"status": "Scan stopped"}
@@ -90,7 +79,7 @@ async def scan_csv(
maxBudget: int = Query(10_000), maxBudget: int = Query(10_000),
enableMultiStepAttack: bool = Query(False), enableMultiStepAttack: bool = Query(False),
secrets: InMemorySecrets = Depends(get_in_memory_secrets), secrets: InMemorySecrets = Depends(get_in_memory_secrets),
) -> StreamingResponse: ):
# TODO: content dataset to fuzzer # TODO: content dataset to fuzzer
content = await file.read() # noqa content = await file.read() # noqa
llm_spec = await llmSpec.read() llm_spec = await llmSpec.read()
+1 -2
View File
@@ -1,8 +1,7 @@
import sentry_sdk import sentry_sdk
from loguru import logger
from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.integrations.logging import ignore_logger
from agentic_security.logutils import logger
from ..primitives import Settings from ..primitives import Settings
@@ -1,6 +1,6 @@
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from agentic_security.app import app from ..app import app
def test_health_check(): def test_health_check():
@@ -5,10 +5,10 @@ import httpx
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from agentic_security.app import app from ..app import app
from agentic_security.primitives import Probe from ..primitives import Probe
from agentic_security.probe_actor.refusal import REFUSAL_MARKS from ..probe_actor.refusal import REFUSAL_MARKS
from agentic_security.probe_data import REGISTRY from ..probe_data import REGISTRY
client = TestClient(app) client = TestClient(app)
@@ -4,7 +4,7 @@ from unittest.mock import patch
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from agentic_security.routes.report import router from .report import router
client = TestClient(router) client = TestClient(router)
@@ -4,8 +4,8 @@ import pytest
from fastapi import HTTPException from fastapi import HTTPException
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from agentic_security.primitives import Settings from ..primitives import Settings
from agentic_security.routes.static import get_static_file, router from .static import get_static_file, router
client = TestClient(router) client = TestClient(router)
+2 -3
View File
@@ -101,7 +101,7 @@ Content-Type: application/json
] ]
`, `,
`POST ${SELF_URL}/v1/self-probe-file `POST ${SELF_URL}/v1/self-probe-file
Authorization: Bearer $API_KEY Authorization: Bearer $GROQ_API_KEY
Content-Type: multipart/form-data Content-Type: multipart/form-data
{ {
@@ -204,9 +204,8 @@ let LLM_CONFIGS = [
{ name: 'Claude', prompts: 40000, logo: '/icons/claude.png' }, { name: 'Claude', prompts: 40000, logo: '/icons/claude.png' },
{ name: 'Cohere', prompts: 40000, logo: '/icons/cohere.png' }, { name: 'Cohere', prompts: 40000, logo: '/icons/cohere.png' },
{ name: 'Azure OpenAI', prompts: 40000, logo: '/icons/azureai.png' }, { name: 'Azure OpenAI', prompts: 40000, logo: '/icons/azureai.png' },
{ name: 'assemblyai', prompts: 40000, logo: fallbackIcon },
{ name: 'OpenRouter.ai', prompts: 40000, logo: '/icons/openrouter.png' }, { name: 'OpenRouter.ai', prompts: 40000, logo: '/icons/openrouter.png' },
{ name: 'assemblyai', prompts: 40000, logo: fallbackIcon },
]; ];
function has_image(spec) { function has_image(spec) {
return spec.includes('<<BASE64_IMAGE>>'); return spec.includes('<<BASE64_IMAGE>>');
-26
View File
@@ -63,32 +63,6 @@
</div> </div>
</section> </section>
</main> </main>
<div class="fixed top-6 right-6 z-50 space-y-3">
<transition-group name="toast">
<div
v-for="toast in toasts"
:key="toast.id"
class="flex items-center p-3 rounded-xl shadow-xl text-white max-w-md animate-toast-in border border-opacity-30"
:class="{
'bg-success-toast border-accent-green': toast.type === 'success',
'bg-error-toast border-accent-red': toast.type === 'error',
'bg-info-toast border-accent-orange': toast.type === 'info'
}"
>
<span class="flex-1 font-medium tracking-wide text-sm">{{ toast.message }}</span>
<button
@click="removeToast(toast.id)"
class="ml-3 focus:outline-none hover:opacity-80 transition-opacity"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</transition-group>
</div>
<main class="max-w-6xl mx-auto space-y-8"> <main class="max-w-6xl mx-auto space-y-8">
<section class="bg-dark-card rounded-lg p-6 shadow-lg" v-show="false"> <section class="bg-dark-card rounded-lg p-6 shadow-lg" v-show="false">
<h2 class="text-2xl font-bold mb-4">Select a Config</h2> <h2 class="text-2xl font-bold mb-4">Select a Config</h2>
+14 -49
View File
@@ -25,8 +25,6 @@ var app = new Vue({
showModules: false, showModules: false,
showLogs: false, showLogs: false,
showConsentModal: true, showConsentModal: true,
toasts: [], // Array to store toast notifications
toastTimeout: 3000, // Duration in milliseconds (3 seconds)
statusDotClass: 'bg-gray-500', // Default status dot class statusDotClass: 'bg-gray-500', // Default status dot class
statusText: 'Verified', // Default status text statusText: 'Verified', // Default status text
statusClass: 'bg-green-500 text-dark-bg', // Default status class statusClass: 'bg-green-500 text-dark-bg', // Default status class
@@ -95,36 +93,21 @@ var app = new Vue({
}, },
methods: { methods: {
showToast(message, type = 'success') {
const id = Date.now(); // Unique ID for each toast
this.toasts.push({ id, message, type });
// Automatically remove toast after timeout
setTimeout(() => {
this.removeToast(id);
}, this.toastTimeout);
},
removeToast(id) {
this.toasts = this.toasts.filter(toast => toast.id !== id);
},
focusTextarea() { focusTextarea() {
this.isFocused = true; this.isFocused = true;
// Remove 'self' assignment if not used elsewhere self = this.$refs;
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.textarea.focus(); // Focus the textarea after rendering
this.adjustHeight({ target: this.$refs.textarea }); self.textarea.focus();
this.adjustHeight({ target: self.textarea });
}); });
// Correct the event listener to use handleOutsideClick document.addEventListener("mousedown", this.handleClickOutside);
document.addEventListener("mousedown", this.handleOutsideClick);
}, },
handleOutsideClick(event) { handleOutsideClick(event) {
if (!this.$refs.textarea) { if (!this.$refs.container.contains(event.target)) {
return
}
if (!this.$refs.textarea.contains(event.target)) {
this.isFocused = false; this.isFocused = false;
document.removeEventListener("mousedown", this.handleOutsideClick); document.removeEventListener("mousedown", this.handleClickOutside);
} }
}, },
unfocusTextarea() { unfocusTextarea() {
@@ -132,12 +115,7 @@ var app = new Vue({
}, },
acceptConsent() { acceptConsent() {
this.showConsentModal = false; // Close the modal this.showConsentModal = false; // Close the modal
try {
localStorage.setItem('consentGiven', 'true'); // Save consent to local storage localStorage.setItem('consentGiven', 'true'); // Save consent to local storage
} catch (e) {
this.showToast('Failed to save consent', 'error'); // Show error if saving fails
}
}, },
saveStateToLocalStorage() { saveStateToLocalStorage() {
@@ -178,7 +156,6 @@ var app = new Vue({
this.integrationVerified = false; this.integrationVerified = false;
this.showResetConfirmation = false; this.showResetConfirmation = false;
this.enableMultiStepAttack = false; this.enableMultiStepAttack = false;
this.showToast('All settings have been reset to default', 'info');
}, },
confirmResetState() { confirmResetState() {
this.showResetConfirmation = true; this.showResetConfirmation = true;
@@ -217,8 +194,6 @@ var app = new Vue({
spec: this.modelSpec, spec: this.modelSpec,
}; };
let startTime = performance.now(); // Capture start time let startTime = performance.now(); // Capture start time
try {
const response = await fetch(`${SELF_URL}/verify`, { const response = await fetch(`${SELF_URL}/verify`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -226,30 +201,24 @@ var app = new Vue({
}, },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
console.log(response);
let r = await response.json(); let r = await response.json();
let endTime = performance.now(); // Capture end time let endTime = performance.now(); // Capture end time
let latency = ((endTime - startTime) / 1000).toFixed(3); // Calculate latency in milliseconds let latency = endTime - startTime; // Calculate latency in milliseconds
latency = latency.toFixed(3) / 1000; // Round to 2 decimal places
this.latency = latency; this.latency = latency;
if (!response.ok) { if (!response.ok) {
this.updateStatusDot(false); this.updateStatusDot(false);
this.errorMsg = 'Integration verification failed:' + JSON.stringify(r); this.errorMsg = 'Integration verification failed:' + JSON.stringify(r);
this.showToast('Integration verification failed', 'error');
} else { } else {
this.errorMsg = ''; this.errorMsg = '';
this.updateStatusDot(true); this.updateStatusDot(true);
this.okMsg = 'Integration verified'; this.okMsg = 'Integration verified';
this.showToast('Integration verified successfully', 'success');
this.integrationVerified = true; this.integrationVerified = true;
} // console.log('Integration verified', this.integrationVerified);
} catch (error) { // this.$forceUpdate();
this.updateStatusDot(true);
this.errorMsg = 'Server unreachable';
this.showToast('Network error', 'error');
}
}
this.saveStateToLocalStorage(); this.saveStateToLocalStorage();
}, },
loadConfigs: async function () { loadConfigs: async function () {
@@ -271,7 +240,6 @@ var app = new Vue({
this.errorMsg = ''; this.errorMsg = '';
this.okMsg = ''; this.okMsg = '';
this.integrationVerified = false; this.integrationVerified = false;
this.showToast(`Config ${index + 1} selected`, 'info');
}, },
toggleModules() { toggleModules() {
this.showModules = !this.showModules; this.showModules = !this.showModules;
@@ -359,7 +327,6 @@ var app = new Vue({
return return
} }
console.log('New row'); console.log('New row');
this.showToast('New module', 'success');
let payload = { let payload = {
table: this.mainTable, table: this.mainTable,
}; };
@@ -470,8 +437,6 @@ var app = new Vue({
} }
}); });
} }
this.scanRunning = false;
this.showToast('Scan finished successfully', 'success');
this.saveStateToLocalStorage(); this.saveStateToLocalStorage();
} }
+2 -54
View File
@@ -19,7 +19,7 @@
technopollas: ['Technopollas', 'sans-serif'], technopollas: ['Technopollas', 'sans-serif'],
}, },
colors: { colors: {
dark: { t1: {
bg: '#0D0D0D', // Jet Black bg: '#0D0D0D', // Jet Black
card: '#1A1A1A', // Dark Carbon Fiber card: '#1A1A1A', // Dark Carbon Fiber
text: '#FFFFFF', text: '#FFFFFF',
@@ -30,7 +30,7 @@
yellow: '#2E4053', // Jet Black yellow: '#2E4053', // Jet Black
}, },
}, },
dark1: { dark: {
bg: '#121212', bg: '#121212',
card: '#1E1E1E', card: '#1E1E1E',
text: '#FFFFFF', text: '#FFFFFF',
@@ -86,7 +86,6 @@
} }
} }
</script> </script>
<style> <style>
.scrollbar-hide::-webkit-scrollbar { .scrollbar-hide::-webkit-scrollbar {
display: none; display: none;
@@ -97,55 +96,4 @@
} }
</style> </style>
<style>
/* Toast-specific colors */
.bg-success-toast {
background: #1C3F74
}
.bg-error-toast {
background: #85144B
}
.bg-info-toast {
background: #FFC300
}
.border-accent-green, .border-accent-red, .border-accent-orange {
border-color: rgba(255, 255, 255, 0.1); /* Subtle white border for depth */
}
/* Animation */
.animate-toast-in {
animation: tSlideIn 0.4s cubic-bezier(0.25, 0.1, 0.25, 1);
}
@keyframes tSlideIn {
from {
transform: translateX(120%) scale(0.95);
opacity: 0;
}
to {
transform: translateX(0) scale(1);
opacity: 1;
}
}
.toast-enter-active,
.toast-leave-active {
transition: all 0.4s cubic-bezier(0.25, 0.1, 0.25, 1);
}
.toast-enter-from,
.toast-leave-to {
transform: translateX(120%) scale(0.95);
opacity: 0;
}
/* Hover effect */
[toast-type]:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6);
}
</style>
</head> </head>
@@ -8,7 +8,7 @@ import time
import pytest import pytest
import agentic_security.test_spec_assets as test_spec_assets import agentic_security.test_spec_assets as test_spec_assets
from agentic_security.lib import SecurityScanner from agentic_security.lib import AgenticSecurity
def has_module(module_name): def has_module(module_name):
@@ -57,7 +57,7 @@ class TestLibraryLevel:
maxBudget = 1000000 maxBudget = 1000000
max_th = 0.3 max_th = 0.3
datasets = make_test_registry() datasets = make_test_registry()
result = SecurityScanner.scan(llmSpec, maxBudget, datasets, max_th) result = AgenticSecurity.scan(llmSpec, maxBudget, datasets, max_th)
assert isinstance(result, dict) assert isinstance(result, dict)
print(result) print(result)
assert len(result) in [0, 1] assert len(result) in [0, 1]
@@ -68,7 +68,7 @@ class TestLibraryLevel:
maxBudget = 1000 maxBudget = 1000
max_th = 0.3 max_th = 0.3
datasets = make_test_registry() datasets = make_test_registry()
result = SecurityScanner.scan( result = AgenticSecurity.scan(
llmSpec, maxBudget, datasets, max_th, enableMultiStepAttack=True llmSpec, maxBudget, datasets, max_th, enableMultiStepAttack=True
) )
assert isinstance(result, dict) assert isinstance(result, dict)
@@ -93,7 +93,7 @@ class TestLibraryLevel:
"opts": {"port": 9094}, "opts": {"port": 9094},
}, },
] ]
result = SecurityScanner.scan(llmSpec, maxBudget, datasets, max_th) result = AgenticSecurity.scan(llmSpec, maxBudget, datasets, max_th)
assert isinstance(result, dict) assert isinstance(result, dict)
print(result) print(result)
assert len(result) in [0, 1] assert len(result) in [0, 1]
@@ -120,7 +120,7 @@ class TestLibraryLevel:
"modality": "text", "modality": "text",
}, },
] ]
result = SecurityScanner.scan(llmSpec, maxBudget, datasets, max_th) result = AgenticSecurity.scan(llmSpec, maxBudget, datasets, max_th)
assert isinstance(result, dict) assert isinstance(result, dict)
print(result) print(result)
assert len(result) in [0, 1] assert len(result) in [0, 1]
@@ -148,7 +148,7 @@ class TestLibraryLevel:
"modality": "text", "modality": "text",
}, },
] ]
result = SecurityScanner.scan(llmSpec, maxBudget, datasets, max_th) result = AgenticSecurity.scan(llmSpec, maxBudget, datasets, max_th)
assert isinstance(result, dict) assert isinstance(result, dict)
print(result) print(result)
assert len(result) in [0, 1] assert len(result) in [0, 1]
@@ -164,10 +164,10 @@ class TestEntrypointCI:
temp_path = os.path.join(tmpdir, "custom_agesec.toml") temp_path = os.path.join(tmpdir, "custom_agesec.toml")
# Override default_path to the temporary path # Override default_path to the temporary path
SecurityScanner.default_path = temp_path AgenticSecurity.default_path = temp_path
# Generate the default configuration # Generate the default configuration
security = SecurityScanner() security = AgenticSecurity()
security.generate_default_settings() security.generate_default_settings()
# Check that the config file was created at the temporary path # Check that the config file was created at the temporary path
@@ -189,17 +189,17 @@ class TestEntrypointCI:
temp_path = os.path.join(tmpdir, "custom_agesec.toml") temp_path = os.path.join(tmpdir, "custom_agesec.toml")
# Override default_path to the temporary path # Override default_path to the temporary path
SecurityScanner.default_path = temp_path AgenticSecurity.default_path = temp_path
# Generate the default configuration # Generate the default configuration
security = SecurityScanner() security = AgenticSecurity()
security.generate_default_settings() security.generate_default_settings()
# Load the generated configuration # Load the generated configuration
SecurityScanner.load_config(temp_path) AgenticSecurity.load_config(temp_path)
# Validate loaded configuration # Validate loaded configuration
config = SecurityScanner.config config = AgenticSecurity.config
assert ( assert (
config["general"]["maxBudget"] == 1000000 config["general"]["maxBudget"] == 1000000
), "maxBudget should be 1000000" ), "maxBudget should be 1000000"
+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/
+5 -1
View File
@@ -33,7 +33,7 @@ The `LLMSpec` class is the core of the HTTP specification. It provides the follo
### Methods ### Methods
- **`from_string(http_spec: str) -> LLMSpec`**: Parses an HTTP specification string into an `LLMSpec` object. - **`from_string(http_spec: str) -> LLMSpec`**: Parses an HTTP specification string into an `LLMSpec` object.
- **`validate(prompt: str, encoded_image: str, encoded_audio: str, files: dict) -> null`**: Validates the request parameters based on the specified modality. - **`validate(prompt: str, encoded_image: str, encoded_audio: str, files: dict) -> None`**: Validates the request parameters based on the specified modality.
- **`probe(prompt: str, encoded_image: str = "", encoded_audio: str = "", files: dict = {}) -> httpx.Response`**: Sends an HTTP request using the specified parameters. - **`probe(prompt: str, encoded_image: str = "", encoded_audio: str = "", files: dict = {}) -> httpx.Response`**: Sends an HTTP request using the specified parameters.
- **`verify() -> httpx.Response`**: Verifies the HTTP specification by sending a test request. - **`verify() -> httpx.Response`**: Verifies the HTTP specification by sending a test request.
@@ -57,6 +57,7 @@ Content-Type: application/json
"temperature": 0.7 "temperature": 0.7
} }
""" """
spec = LLMSpec.from_string(http_spec) spec = LLMSpec.from_string(http_spec)
response = await spec.probe("What is the capital of France?") response = await spec.probe("What is the capital of France?")
``` ```
@@ -75,6 +76,7 @@ Content-Type: application/json
"temperature": 0.7 "temperature": 0.7
} }
""" """
spec = LLMSpec.from_string(http_spec) spec = LLMSpec.from_string(http_spec)
encoded_image = encode_image_base64_by_url("https://example.com/image.jpg") encoded_image = encode_image_base64_by_url("https://example.com/image.jpg")
response = await spec.probe("What is in this image?", encoded_image=encoded_image) response = await spec.probe("What is in this image?", encoded_image=encoded_image)
@@ -94,6 +96,7 @@ Content-Type: application/json
"temperature": 0.7 "temperature": 0.7
} }
""" """
spec = LLMSpec.from_string(http_spec) spec = LLMSpec.from_string(http_spec)
encoded_audio = encode_audio_base64_by_url("https://example.com/audio.mp3") encoded_audio = encode_audio_base64_by_url("https://example.com/audio.mp3")
response = await spec.probe("Transcribe this audio:", encoded_audio=encoded_audio) response = await spec.probe("Transcribe this audio:", encoded_audio=encoded_audio)
@@ -113,6 +116,7 @@ Content-Type: multipart/form-data
"temperature": 0.7 "temperature": 0.7
} }
""" """
spec = LLMSpec.from_string(http_spec) spec = LLMSpec.from_string(http_spec)
files = {"file": ("document.txt", open("document.txt", "rb"))} files = {"file": ("document.txt", open("document.txt", "rb"))}
response = await spec.probe("Process this file:", files=files) response = await spec.probe("Process this file:", files=files)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 MiB

+1 -1
View File
@@ -21,4 +21,4 @@ Note: Please be aware that Agentic Security is designed as a safety scanner tool
## UI 🧙 ## UI 🧙
<img width="100%" alt="booking-screen" src="https://res.cloudinary.com/dq0w2rtm9/image/upload/v1741192668/final_aa9jhb.gif"> <img width="100%" alt="booking-screen" src="https://res.cloudinary.com/dq0w2rtm9/image/upload/v1736433557/z0bsyzhsqlgcr3w4ovwp.gif">
+14 -4
View File
@@ -54,15 +54,20 @@ The `probe_data` module is a core component of the Agentic Security project, res
- **Classes:** - **Classes:**
- `PromptSelectionInterface`: Abstract base class for prompt selection strategies. - `PromptSelectionInterface`: Abstract base class for prompt selection strategies.
- Methods: - Methods:
- `select_next_prompt(current_prompt: str, passed_guard: bool) -> str`: Selects next prompt - `select_next_prompt(current_prompt: str, passed_guard: bool) -> str`: Selects next prompt
- `select_next_prompts(current_prompt: str, passed_guard: bool) -> list[str]`: Selects multiple prompts - `select_next_prompts(current_prompt: str, passed_guard: bool) -> list[str]`: Selects multiple prompts
- `update_rewards(previous_prompt: str, current_prompt: str, reward: float, passed_guard: bool) -> null`: Updates rewards - `update_rewards(previous_prompt: str, current_prompt: str, reward: float, passed_guard: bool) -> None`: Updates rewards
- `RandomPromptSelector`: Basic random selection with history tracking. - `RandomPromptSelector`: Basic random selection with history tracking.
- Parameters: - Parameters:
- `prompts: list[str]`: List of available prompts - `prompts: list[str]`: List of available prompts
- `history_size: int = 3`: Size of history to prevent cycles - `history_size: int = 3`: Size of history to prevent cycles
- `CloudRLPromptSelector`: Cloud-based RL implementation with fallback. - `CloudRLPromptSelector`: Cloud-based RL implementation with fallback.
- Parameters: - Parameters:
- `prompts: list[str]`: List of available prompts - `prompts: list[str]`: List of available prompts
- `api_url: str`: URL of RL service - `api_url: str`: URL of RL service
@@ -70,7 +75,9 @@ The `probe_data` module is a core component of the Agentic Security project, res
- `history_size: int = 300`: Size of history - `history_size: int = 300`: Size of history
- `timeout: int = 5`: Request timeout - `timeout: int = 5`: Request timeout
- `run_id: str = ""`: Unique run identifier - `run_id: str = ""`: Unique run identifier
- `QLearningPromptSelector`: Local Q-learning implementation. - `QLearningPromptSelector`: Local Q-learning implementation.
- Parameters: - Parameters:
- `prompts: list[str]`: List of available prompts - `prompts: list[str]`: List of available prompts
- `learning_rate: float = 0.1`: Learning rate - `learning_rate: float = 0.1`: Learning rate
@@ -79,7 +86,9 @@ The `probe_data` module is a core component of the Agentic Security project, res
- `exploration_decay: float = 0.995`: Exploration decay rate - `exploration_decay: float = 0.995`: Exploration decay rate
- `min_exploration: float = 0.01`: Minimum exploration rate - `min_exploration: float = 0.01`: Minimum exploration rate
- `history_size: int = 300`: Size of history - `history_size: int = 300`: Size of history
- **Module**: Main class that uses CloudRLPromptSelector.
- `Module`: Main class that uses CloudRLPromptSelector.
- Parameters: - Parameters:
- `prompt_groups: list[str]`: Groups of prompts - `prompt_groups: list[str]`: Groups of prompts
- `tools_inbox: asyncio.Queue`: Queue for tool communication - `tools_inbox: asyncio.Queue`: Queue for tool communication
@@ -110,9 +119,10 @@ from agentic_security.probe_data.modules.rl_model import QLearningPromptSelector
prompts = ["What is AI?", "Explain machine learning"] prompts = ["What is AI?", "Explain machine learning"]
selector = QLearningPromptSelector(prompts) selector = QLearningPromptSelector(prompts)
current_prompt = "What is AI?" current_prompt = "What is AI?"
next_prompt = selector.select_next_prompt(current_prompt, passed_guard=true) next_prompt = selector.select_next_prompt(current_prompt, passed_guard=True)
selector.update_rewards(current_prompt, next_prompt, reward=1.0, passed_guard=true) selector.update_rewards(current_prompt, next_prompt, reward=1.0, passed_guard=True)
``` ```
## Conclusion ## Conclusion
+9 -13
View File
@@ -1,16 +1,14 @@
:root { :root {
--md-primary-fg-color: #2E4053; --md-primary-fg-color: #e92063;
/* Primary color changed to pinkish */ --md-primary-fg-color--light: #e92063;
--md-primary-fg-color--light: #E0A3B6; --md-primary-fg-color--dark: #e92063;
--md-primary-fg-color--dark: #1C3F74;
/* Dark variant changed to blue */
} }
/* Updated slate color scheme with new background */
/* Revert hue value to that of pre mkdocs-material v9.4.0 */
[data-md-color-scheme="slate"] { [data-md-color-scheme="slate"] {
--md-hue: 230; --md-hue: 230;
--md-default-bg-color: #1A1A1A; --md-default-bg-color: hsla(230, 15%, 21%, 1);
/* Background changed to dark gray */
} }
.hide { .hide {
@@ -26,15 +24,12 @@ img.index-header {
max-width: 500px; max-width: 500px;
} }
/* Updated custom colors */
.pydantic-pink { .pydantic-pink {
color: #E0A3B6; color: #FF007F;
/* Updated to match new theme */
} }
.team-blue { .team-blue {
color: #1C3F74; color: #0072CE;
/* Updated to match new theme */
} }
.secure-green { .secure-green {
@@ -72,6 +67,7 @@ img.index-header {
text-align: center; text-align: center;
} }
/* Hide the entire footer */ /* Hide the entire footer */
.md-footer { .md-footer {
display: none; display: none;
+1 -1
View File
@@ -89,7 +89,7 @@ theme:
name: Switch to light mode name: Switch to light mode
icon: icon:
repo: fontawesome/brands/github repo: fontawesome/brands/github
favicon: https://res.cloudinary.com/dq0w2rtm9/image/upload/v1741195421/favicon_kuz6xr.png favicon: "https://res.cloudinary.com/dq0w2rtm9/image/upload/v1737555066/r17hrkre246doczwmvbv.png"
extra: extra:
generator: false generator: false
Generated
+362 -384
View File
@@ -149,24 +149,23 @@ files = [
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.9.0" version = "4.4.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations" description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.8"
files = [ files = [
{file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
{file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
] ]
[package.dependencies] [package.dependencies]
idna = ">=2.8" idna = ">=2.8"
sniffio = ">=1.1" sniffio = ">=1.1"
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras] [package.extras]
doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (>=0.26.1)"] trio = ["trio (>=0.23)"]
[[package]] [[package]]
name = "appnope" name = "appnope"
@@ -230,24 +229,6 @@ files = [
[package.extras] [package.extras]
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] 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]] [[package]]
name = "beautifulsoup4" name = "beautifulsoup4"
version = "4.12.3" version = "4.12.3"
@@ -664,13 +645,13 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"]
[[package]] [[package]]
name = "datasets" name = "datasets"
version = "3.4.0" version = "3.3.0"
description = "HuggingFace community-driven open-source library of datasets" description = "HuggingFace community-driven open-source library of datasets"
optional = false optional = false
python-versions = ">=3.9.0" python-versions = ">=3.9.0"
files = [ files = [
{file = "datasets-3.4.0-py3-none-any.whl", hash = "sha256:35ef5182bddd38f7aa774d9f33c3e8b8e9c9c7ea41b4b7969fde431919cb556b"}, {file = "datasets-3.3.0-py3-none-any.whl", hash = "sha256:22312d09626f8fc3aa0a237b0c164997f5903bddd4c4c9e27dbaf563754c681b"},
{file = "datasets-3.4.0.tar.gz", hash = "sha256:f3defae5d9c79ff586db3b17389fdde01704ffea015293a050d7e8ab6816bad8"}, {file = "datasets-3.3.0.tar.gz", hash = "sha256:54c607b06f6eaa1572e21e200d2870d89d50e3bcc622dc2021a53a6ce4f684c2"},
] ]
[package.dependencies] [package.dependencies]
@@ -692,15 +673,15 @@ xxhash = "*"
[package.extras] [package.extras]
audio = ["librosa", "soundfile (>=0.12.1)", "soxr (>=0.4.0)"] audio = ["librosa", "soundfile (>=0.12.1)", "soxr (>=0.4.0)"]
benchmarks = ["tensorflow (==2.12.0)", "torch (==2.0.1)", "transformers (==4.30.1)"] benchmarks = ["tensorflow (==2.12.0)", "torch (==2.0.1)", "transformers (==4.30.1)"]
dev = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (>=7.17.12,<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyav", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "ruff (>=0.3.0)", "s3fs", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0)", "tensorflow (>=2.6.0)", "tensorflow (>=2.6.0)", "tiktoken", "torch", "torch (>=2.0.0)", "torchdata", "torchvision", "transformers", "transformers (>=4.42.0)", "zstandard"] dev = ["Pillow (>=9.4.0)", "absl-py", "decorator", "decord (==0.6.0)", "elasticsearch (>=7.17.12,<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "ruff (>=0.3.0)", "s3fs", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0)", "tensorflow (>=2.6.0)", "tensorflow (>=2.6.0)", "tiktoken", "torch", "torch (>=2.0.0)", "torchdata", "transformers", "transformers (>=4.42.0)", "zstandard"]
docs = ["s3fs", "tensorflow (>=2.6.0)", "torch", "transformers"] docs = ["s3fs", "tensorflow (>=2.6.0)", "torch", "transformers"]
jax = ["jax (>=0.3.14)", "jaxlib (>=0.3.14)"] jax = ["jax (>=0.3.14)", "jaxlib (>=0.3.14)"]
quality = ["ruff (>=0.3.0)"] quality = ["ruff (>=0.3.0)"]
s3 = ["s3fs"] s3 = ["s3fs"]
tensorflow = ["tensorflow (>=2.6.0)"] tensorflow = ["tensorflow (>=2.6.0)"]
tensorflow-gpu = ["tensorflow (>=2.6.0)"] tensorflow-gpu = ["tensorflow (>=2.6.0)"]
tests = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (>=7.17.12,<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyav", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0)", "tensorflow (>=2.6.0)", "tiktoken", "torch (>=2.0.0)", "torchdata", "torchvision", "transformers (>=4.42.0)", "zstandard"] tests = ["Pillow (>=9.4.0)", "absl-py", "decorator", "decord (==0.6.0)", "elasticsearch (>=7.17.12,<8.0.0)", "faiss-cpu (>=1.8.0.post1)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tensorflow (>=2.16.0)", "tensorflow (>=2.6.0)", "tiktoken", "torch (>=2.0.0)", "torchdata", "transformers (>=4.42.0)", "zstandard"]
tests-numpy2 = ["Pillow (>=9.4.0)", "absl-py", "decorator", "elasticsearch (>=7.17.12,<8.0.0)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyav", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tiktoken", "torch (>=2.0.0)", "torchdata", "torchvision", "transformers (>=4.42.0)", "zstandard"] tests-numpy2 = ["Pillow (>=9.4.0)", "absl-py", "decorator", "decord (==0.6.0)", "elasticsearch (>=7.17.12,<8.0.0)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "lz4", "moto[server]", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "soundfile (>=0.12.1)", "soxr (>=0.4.0)", "sqlalchemy", "tiktoken", "torch (>=2.0.0)", "torchdata", "transformers (>=4.42.0)", "zstandard"]
torch = ["torch"] torch = ["torch"]
vision = ["Pillow (>=9.4.0)"] vision = ["Pillow (>=9.4.0)"]
@@ -787,20 +768,6 @@ files = [
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, {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]] [[package]]
name = "executing" name = "executing"
version = "2.2.0" version = "2.2.0"
@@ -1163,26 +1130,15 @@ http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"] socks = ["socksio (==1.*)"]
zstd = ["zstandard (>=0.18.0)"] zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "httpx-sse"
version = "0.4.0"
description = "Consume Server-Sent Event (SSE) messages with HTTPX."
optional = false
python-versions = ">=3.8"
files = [
{file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"},
{file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"},
]
[[package]] [[package]]
name = "huggingface-hub" name = "huggingface-hub"
version = "0.29.3" version = "0.28.1"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "huggingface_hub-0.29.3-py3-none-any.whl", hash = "sha256:0b25710932ac649c08cdbefa6c6ccb8e88eef82927cacdb048efb726429453aa"}, {file = "huggingface_hub-0.28.1-py3-none-any.whl", hash = "sha256:aa6b9a3ffdae939b72c464dbb0d7f99f56e649b55c3d52406f49e0a5a620c0a7"},
{file = "huggingface_hub-0.29.3.tar.gz", hash = "sha256:64519a25716e0ba382ba2d3fb3ca082e7c7eb4a2fc634d200e8380006e0760e5"}, {file = "huggingface_hub-0.28.1.tar.gz", hash = "sha256:893471090c98e3b6efbdfdacafe4052b20b84d59866fb6f54c33d9af18c303ae"},
] ]
[package.dependencies] [package.dependencies]
@@ -1246,13 +1202,13 @@ files = [
[[package]] [[package]]
name = "inline-snapshot" name = "inline-snapshot"
version = "0.20.9" version = "0.20.3"
description = "golden master/snapshot/approval testing library which puts the values right into your source code" description = "golden master/snapshot/approval testing library which puts the values right into your source code"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "inline_snapshot-0.20.9-py3-none-any.whl", hash = "sha256:e4bffcfb39d79bba0320a138e47217601823bbf6948760d75fa175d62a7b893d"}, {file = "inline_snapshot-0.20.3-py3-none-any.whl", hash = "sha256:1ea999fbf38dd11cc72d0e1a0b9303c63d496b77bdc406a394fe2424ae842f70"},
{file = "inline_snapshot-0.20.9.tar.gz", hash = "sha256:ce41505690f0f6ca96466776bc3e015ccc7412ecb65b74f4e7e90f1a834cbac4"}, {file = "inline_snapshot-0.20.3.tar.gz", hash = "sha256:7a353170b7e42aa89086c7ba790a973c9645523acf985532648dabd7ee2d71f2"},
] ]
[package.dependencies] [package.dependencies]
@@ -1355,13 +1311,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.6" version = "3.1.5"
description = "A very fast and expressive template engine." description = "A very fast and expressive template engine."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
] ]
[package.dependencies] [package.dependencies]
@@ -1739,45 +1695,45 @@ files = [
[[package]] [[package]]
name = "matplotlib" name = "matplotlib"
version = "3.10.1" version = "3.10.0"
description = "Python plotting package" description = "Python plotting package"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
files = [ files = [
{file = "matplotlib-3.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16"}, {file = "matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6"},
{file = "matplotlib-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2"}, {file = "matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e"},
{file = "matplotlib-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0673b4b8f131890eb3a1ad058d6e065fb3c6e71f160089b65f8515373394698"}, {file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5"},
{file = "matplotlib-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e875b95ac59a7908978fe307ecdbdd9a26af7fa0f33f474a27fcf8c99f64a19"}, {file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6"},
{file = "matplotlib-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2589659ea30726284c6c91037216f64a506a9822f8e50592d48ac16a2f29e044"}, {file = "matplotlib-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1"},
{file = "matplotlib-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a97ff127f295817bc34517255c9db6e71de8eddaab7f837b7d341dee9f2f587f"}, {file = "matplotlib-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3"},
{file = "matplotlib-3.10.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:057206ff2d6ab82ff3e94ebd94463d084760ca682ed5f150817b859372ec4401"}, {file = "matplotlib-3.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fd44fc75522f58612ec4a33958a7e5552562b7705b42ef1b4f8c0818e304a363"},
{file = "matplotlib-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a144867dd6bf8ba8cb5fc81a158b645037e11b3e5cf8a50bd5f9917cb863adfe"}, {file = "matplotlib-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c58a9622d5dbeb668f407f35f4e6bfac34bb9ecdcc81680c04d0258169747997"},
{file = "matplotlib-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56c5d9fcd9879aa8040f196a235e2dcbdf7dd03ab5b07c0696f80bc6cf04bedd"}, {file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:845d96568ec873be63f25fa80e9e7fae4be854a66a7e2f0c8ccc99e94a8bd4ef"},
{file = "matplotlib-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f69dc9713e4ad2fb21a1c30e37bd445d496524257dfda40ff4a8efb3604ab5c"}, {file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5439f4c5a3e2e8eab18e2f8c3ef929772fd5641876db71f08127eed95ab64683"},
{file = "matplotlib-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c59af3e8aca75d7744b68e8e78a669e91ccbcf1ac35d0102a7b1b46883f1dd7"}, {file = "matplotlib-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4673ff67a36152c48ddeaf1135e74ce0d4bce1bbf836ae40ed39c29edf7e2765"},
{file = "matplotlib-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:11b65088c6f3dae784bc72e8d039a2580186285f87448babb9ddb2ad0082993a"}, {file = "matplotlib-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e8632baebb058555ac0cde75db885c61f1212e47723d63921879806b40bec6a"},
{file = "matplotlib-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:66e907a06e68cb6cfd652c193311d61a12b54f56809cafbed9736ce5ad92f107"}, {file = "matplotlib-3.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4659665bc7c9b58f8c00317c3c2a299f7f258eeae5a5d56b4c64226fca2f7c59"},
{file = "matplotlib-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b4bb156abb8fa5e5b2b460196f7db7264fc6d62678c03457979e7d5254b7be"}, {file = "matplotlib-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d44cb942af1693cced2604c33a9abcef6205601c445f6d0dc531d813af8a2f5a"},
{file = "matplotlib-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1985ad3d97f51307a2cbfc801a930f120def19ba22864182dacef55277102ba6"}, {file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a994f29e968ca002b50982b27168addfd65f0105610b6be7fa515ca4b5307c95"},
{file = "matplotlib-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96f2c2f825d1257e437a1482c5a2cf4fee15db4261bd6fc0750f81ba2b4ba3d"}, {file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0558bae37f154fffda54d779a592bc97ca8b4701f1c710055b609a3bac44c8"},
{file = "matplotlib-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35e87384ee9e488d8dd5a2dd7baf471178d38b90618d8ea147aced4ab59c9bea"}, {file = "matplotlib-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:503feb23bd8c8acc75541548a1d709c059b7184cde26314896e10a9f14df5f12"},
{file = "matplotlib-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfd414bce89cc78a7e1d25202e979b3f1af799e416010a20ab2b5ebb3a02425c"}, {file = "matplotlib-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:c40ba2eb08b3f5de88152c2333c58cee7edcead0a2a0d60fcafa116b17117adc"},
{file = "matplotlib-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b"}, {file = "matplotlib-3.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96f2886f5c1e466f21cc41b70c5a0cd47bfa0015eb2d5793c88ebce658600e25"},
{file = "matplotlib-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1"}, {file = "matplotlib-3.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:12eaf48463b472c3c0f8dbacdbf906e573013df81a0ab82f0616ea4b11281908"},
{file = "matplotlib-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3"}, {file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fbbabc82fde51391c4da5006f965e36d86d95f6ee83fb594b279564a4c5d0d2"},
{file = "matplotlib-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6"}, {file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2e15300530c1a94c63cfa546e3b7864bd18ea2901317bae8bbf06a5ade6dcf"},
{file = "matplotlib-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b"}, {file = "matplotlib-3.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3547d153d70233a8496859097ef0312212e2689cdf8d7ed764441c77604095ae"},
{file = "matplotlib-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473"}, {file = "matplotlib-3.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c55b20591ced744aa04e8c3e4b7543ea4d650b6c3c4b208c08a05b4010e8b442"},
{file = "matplotlib-3.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01"}, {file = "matplotlib-3.10.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ade1003376731a971e398cc4ef38bb83ee8caf0aee46ac6daa4b0506db1fd06"},
{file = "matplotlib-3.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb"}, {file = "matplotlib-3.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95b710fea129c76d30be72c3b38f330269363fbc6e570a5dd43580487380b5ff"},
{file = "matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972"}, {file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdbaf909887373c3e094b0318d7ff230b2ad9dcb64da7ade654182872ab2593"},
{file = "matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3"}, {file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d907fddb39f923d011875452ff1eca29a9e7f21722b873e90db32e5d8ddff12e"},
{file = "matplotlib-3.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f"}, {file = "matplotlib-3.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3b427392354d10975c1d0f4ee18aa5844640b512d5311ef32efd4dd7db106ede"},
{file = "matplotlib-3.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9"}, {file = "matplotlib-3.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5fd41b0ec7ee45cd960a8e71aea7c946a28a0b8a4dcee47d2856b2af051f334c"},
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:648406f1899f9a818cef8c0231b44dcfc4ff36f167101c3fd1c9151f24220fdc"}, {file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03"},
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:02582304e352f40520727984a5a18f37e8187861f954fea9be7ef06569cf85b4"}, {file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea"},
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3809916157ba871bcdd33d3493acd7fe3037db5daa917ca6e77975a94cef779"}, {file = "matplotlib-3.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef"},
{file = "matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba"}, {file = "matplotlib-3.10.0.tar.gz", hash = "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278"},
] ]
[package.dependencies] [package.dependencies]
@@ -1808,32 +1764,6 @@ files = [
[package.dependencies] [package.dependencies]
traitlets = "*" traitlets = "*"
[[package]]
name = "mcp"
version = "1.6.0"
description = "Model Context Protocol SDK"
optional = false
python-versions = ">=3.10"
files = [
{file = "mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0"},
{file = "mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723"},
]
[package.dependencies]
anyio = ">=4.5"
httpx = ">=0.27"
httpx-sse = ">=0.4"
pydantic = ">=2.7.2,<3.0.0"
pydantic-settings = ">=2.5.2"
sse-starlette = ">=1.6.1"
starlette = ">=0.27"
uvicorn = ">=0.23.1"
[package.extras]
cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"]
rich = ["rich (>=13.9.4)"]
ws = ["websockets (>=15.0.1)"]
[[package]] [[package]]
name = "mdit-py-plugins" name = "mdit-py-plugins"
version = "0.4.2" version = "0.4.2"
@@ -1918,13 +1848,13 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
[[package]] [[package]]
name = "mkdocs-autorefs" name = "mkdocs-autorefs"
version = "1.4.0" version = "1.3.0"
description = "Automatically link across pages in MkDocs." description = "Automatically link across pages in MkDocs."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "mkdocs_autorefs-1.4.0-py3-none-any.whl", hash = "sha256:bad19f69655878d20194acd0162e29a89c3f7e6365ffe54e72aa3fd1072f240d"}, {file = "mkdocs_autorefs-1.3.0-py3-none-any.whl", hash = "sha256:d180f9778a04e78b7134e31418f238bba56f56d6a8af97873946ff661befffb3"},
{file = "mkdocs_autorefs-1.4.0.tar.gz", hash = "sha256:a9c0aa9c90edbce302c09d050a3c4cb7c76f8b7b2c98f84a7a05f53d00392156"}, {file = "mkdocs_autorefs-1.3.0.tar.gz", hash = "sha256:6867764c099ace9025d6ac24fd07b85a98335fbd30107ef01053697c8f46db61"},
] ]
[package.dependencies] [package.dependencies]
@@ -1969,18 +1899,17 @@ pygments = ">2.12.0"
[[package]] [[package]]
name = "mkdocs-material" name = "mkdocs-material"
version = "9.6.10" version = "9.6.4"
description = "Documentation that simply works" description = "Documentation that simply works"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "mkdocs_material-9.6.10-py3-none-any.whl", hash = "sha256:36168548df4e2ddeb9a334ddae4ab9c388ccfea4dd50ffee657d22b93dcb1c3e"}, {file = "mkdocs_material-9.6.4-py3-none-any.whl", hash = "sha256:414e8376551def6d644b8e6f77226022868532a792eb2c9accf52199009f568f"},
{file = "mkdocs_material-9.6.10.tar.gz", hash = "sha256:25a453c1f24f34fcf1f53680c03d2c1421b52ce5247f4468153c87a70cd5f1fc"}, {file = "mkdocs_material-9.6.4.tar.gz", hash = "sha256:4d1d35e1c1d3e15294cb7fa5d02e0abaee70d408f75027dc7be6e30fb32e6867"},
] ]
[package.dependencies] [package.dependencies]
babel = ">=2.10,<3.0" babel = ">=2.10,<3.0"
backrefs = ">=5.7.post1,<6.0"
colorama = ">=0.4,<1.0" colorama = ">=0.4,<1.0"
jinja2 = ">=3.0,<4.0" jinja2 = ">=3.0,<4.0"
markdown = ">=3.2,<4.0" markdown = ">=3.2,<4.0"
@@ -1989,6 +1918,7 @@ mkdocs-material-extensions = ">=1.3,<2.0"
paginate = ">=0.5,<1.0" paginate = ">=0.5,<1.0"
pygments = ">=2.16,<3.0" pygments = ">=2.16,<3.0"
pymdown-extensions = ">=10.2,<11.0" pymdown-extensions = ">=10.2,<11.0"
regex = ">=2022.4"
requests = ">=2.26,<3.0" requests = ">=2.26,<3.0"
[package.extras] [package.extras]
@@ -2009,26 +1939,27 @@ files = [
[[package]] [[package]]
name = "mkdocstrings" name = "mkdocstrings"
version = "0.29.0" version = "0.28.1"
description = "Automatic documentation from sources, for MkDocs." description = "Automatic documentation from sources, for MkDocs."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d"}, {file = "mkdocstrings-0.28.1-py3-none-any.whl", hash = "sha256:a5878ae5cd1e26f491ff084c1f9ab995687d52d39a5c558e9b7023d0e4e0b740"},
{file = "mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e"}, {file = "mkdocstrings-0.28.1.tar.gz", hash = "sha256:fb64576906771b7701e8e962fd90073650ff689e95eb86e86751a66d65ab4489"},
] ]
[package.dependencies] [package.dependencies]
Jinja2 = ">=2.11.1" Jinja2 = ">=2.11.1"
Markdown = ">=3.6" Markdown = ">=3.6"
MarkupSafe = ">=1.1" MarkupSafe = ">=1.1"
mkdocs = ">=1.6" mkdocs = ">=1.4"
mkdocs-autorefs = ">=1.4" mkdocs-autorefs = ">=1.3"
mkdocs-get-deps = ">=0.2"
pymdown-extensions = ">=6.3" pymdown-extensions = ">=6.3"
[package.extras] [package.extras]
crystal = ["mkdocstrings-crystal (>=0.3.4)"] crystal = ["mkdocstrings-crystal (>=0.3.4)"]
python = ["mkdocstrings-python (>=1.16.2)"] python = ["mkdocstrings-python (>=0.5.2)"]
python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
[[package]] [[package]]
@@ -2156,43 +2087,49 @@ dill = ">=0.3.8"
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "1.15.0" version = "1.14.1"
description = "Optional static typing for Python" description = "Optional static typing for Python"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.8"
files = [ files = [
{file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"},
{file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"},
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"},
{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.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"},
{file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"},
{file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"},
{file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"},
{file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"},
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"},
{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.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"},
{file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"},
{file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"},
{file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"},
{file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"},
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"},
{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.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"},
{file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"},
{file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"},
{file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"},
{file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"},
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"},
{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.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"},
{file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"},
{file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"},
{file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"},
{file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"},
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"},
{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.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"},
{file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"},
{file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"},
{file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"},
{file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, {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"},
] ]
[package.dependencies] [package.dependencies]
@@ -2320,143 +2257,154 @@ files = [
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.2.3" version = "2.2.2"
description = "Fundamental package for array computing in Python" description = "Fundamental package for array computing in Python"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
files = [ files = [
{file = "numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71"}, {file = "numpy-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e"},
{file = "numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787"}, {file = "numpy-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e"},
{file = "numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716"}, {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715"},
{file = "numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b"}, {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a"},
{file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3"}, {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97"},
{file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52"}, {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957"},
{file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b"}, {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d"},
{file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027"}, {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd"},
{file = "numpy-2.2.3-cp310-cp310-win32.whl", hash = "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094"}, {file = "numpy-2.2.2-cp310-cp310-win32.whl", hash = "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160"},
{file = "numpy-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb"}, {file = "numpy-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014"},
{file = "numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8"}, {file = "numpy-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189"},
{file = "numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b"}, {file = "numpy-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323"},
{file = "numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a"}, {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac"},
{file = "numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636"}, {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e"},
{file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d"}, {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c"},
{file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb"}, {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f"},
{file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2"}, {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826"},
{file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b"}, {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8"},
{file = "numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5"}, {file = "numpy-2.2.2-cp311-cp311-win32.whl", hash = "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50"},
{file = "numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f"}, {file = "numpy-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2"},
{file = "numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d"}, {file = "numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467"},
{file = "numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95"}, {file = "numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a"},
{file = "numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea"}, {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825"},
{file = "numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532"}, {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37"},
{file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e"}, {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748"},
{file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe"}, {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0"},
{file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021"}, {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278"},
{file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8"}, {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba"},
{file = "numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe"}, {file = "numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283"},
{file = "numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d"}, {file = "numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb"},
{file = "numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba"}, {file = "numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc"},
{file = "numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50"}, {file = "numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369"},
{file = "numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1"}, {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd"},
{file = "numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5"}, {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be"},
{file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2"}, {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84"},
{file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1"}, {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff"},
{file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304"}, {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0"},
{file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d"}, {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de"},
{file = "numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693"}, {file = "numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9"},
{file = "numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b"}, {file = "numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890"}, {file = "numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c"}, {file = "numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94"}, {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0"}, {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49"},
{file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610"}, {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2"},
{file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76"}, {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7"},
{file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a"}, {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb"},
{file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf"}, {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648"},
{file = "numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef"}, {file = "numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4"},
{file = "numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082"}, {file = "numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576"},
{file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d"}, {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495"},
{file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9"}, {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df"},
{file = "numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e"}, {file = "numpy-2.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a"},
{file = "numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4"}, {file = "numpy-2.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60"},
{file = "numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020"}, {file = "numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"},
] ]
[[package]] [[package]]
name = "orjson" name = "orjson"
version = "3.10.16" version = "3.10.15"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.8"
files = [ files = [
{file = "orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8"}, {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.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00"}, {file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8"},
{file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c682d852d0ce77613993dc967e90e151899fe2d8e71c20e9be164080f468e370"}, {file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8"},
{file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c520ae736acd2e32df193bcff73491e64c936f3e44a2916b548da048a48b46b"}, {file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814"},
{file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:134f87c76bfae00f2094d85cfab261b289b76d78c6da8a7a3b3c09d362fd1e06"}, {file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164"},
{file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b59afde79563e2cf37cfe62ee3b71c063fd5546c8e662d7fcfc2a3d5031a5c4c"}, {file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf"},
{file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113602f8241daaff05d6fad25bd481d54c42d8d72ef4c831bb3ab682a54d9e15"}, {file = "orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061"},
{file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4fc0077d101f8fab4031e6554fc17b4c2ad8fdbc56ee64a727f3c95b379e31da"}, {file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3"},
{file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9c6bf6ff180cd69e93f3f50380224218cfab79953a868ea3908430bcfaf9cb5e"}, {file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d"},
{file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5673eadfa952f95a7cd76418ff189df11b0a9c34b1995dff43a6fdbce5d63bf4"}, {file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182"},
{file = "orjson-3.10.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5fe638a423d852b0ae1e1a79895851696cb0d9fa0946fdbfd5da5072d9bb9551"}, {file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e"},
{file = "orjson-3.10.16-cp310-cp310-win32.whl", hash = "sha256:33af58f479b3c6435ab8f8b57999874b4b40c804c7a36b5cc6b54d8f28e1d3dd"}, {file = "orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab"},
{file = "orjson-3.10.16-cp310-cp310-win_amd64.whl", hash = "sha256:0338356b3f56d71293c583350af26f053017071836b07e064e92819ecf1aa055"}, {file = "orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806"},
{file = "orjson-3.10.16-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44fcbe1a1884f8bc9e2e863168b0f84230c3d634afe41c678637d2728ea8e739"}, {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.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78177bf0a9d0192e0b34c3d78bcff7fe21d1b5d84aeb5ebdfe0dbe637b885225"}, {file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef"},
{file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12824073a010a754bb27330cad21d6e9b98374f497f391b8707752b96f72e741"}, {file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334"},
{file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddd41007e56284e9867864aa2f29f3136bb1dd19a49ca43c0b4eda22a579cf53"}, {file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d"},
{file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0877c4d35de639645de83666458ca1f12560d9fa7aa9b25d8bb8f52f61627d14"}, {file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0"},
{file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a09a539e9cc3beead3e7107093b4ac176d015bec64f811afb5965fce077a03c"}, {file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13"},
{file = "orjson-3.10.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31b98bc9b40610fec971d9a4d67bb2ed02eec0a8ae35f8ccd2086320c28526ca"}, {file = "orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5"},
{file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ce243f5a8739f3a18830bc62dc2e05b69a7545bafd3e3249f86668b2bcd8e50"}, {file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b"},
{file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64792c0025bae049b3074c6abe0cf06f23c8e9f5a445f4bab31dc5ca23dbf9e1"}, {file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399"},
{file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea53f7e68eec718b8e17e942f7ca56c6bd43562eb19db3f22d90d75e13f0431d"}, {file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388"},
{file = "orjson-3.10.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a741ba1a9488c92227711bde8c8c2b63d7d3816883268c808fbeada00400c164"}, {file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c"},
{file = "orjson-3.10.16-cp311-cp311-win32.whl", hash = "sha256:c7ed2c61bb8226384c3fdf1fb01c51b47b03e3f4536c985078cccc2fd19f1619"}, {file = "orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e"},
{file = "orjson-3.10.16-cp311-cp311-win_amd64.whl", hash = "sha256:cd67d8b3e0e56222a2e7b7f7da9031e30ecd1fe251c023340b9f12caca85ab60"}, {file = "orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e"},
{file = "orjson-3.10.16-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6d3444abbfa71ba21bb042caa4b062535b122248259fdb9deea567969140abca"}, {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.16-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:30245c08d818fdcaa48b7d5b81499b8cae09acabb216fe61ca619876b128e184"}, {file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d"},
{file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0ba1d0baa71bf7579a4ccdcf503e6f3098ef9542106a0eca82395898c8a500a"}, {file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0"},
{file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb0beefa5ef3af8845f3a69ff2a4aa62529b5acec1cfe5f8a6b4141033fd46ef"}, {file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4"},
{file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6daa0e1c9bf2e030e93c98394de94506f2a4d12e1e9dadd7c53d5e44d0f9628e"}, {file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767"},
{file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9da9019afb21e02410ef600e56666652b73eb3e4d213a0ec919ff391a7dd52aa"}, {file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41"},
{file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:daeb3a1ee17b69981d3aae30c3b4e786b0f8c9e6c71f2b48f1aef934f63f38f4"}, {file = "orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514"},
{file = "orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fed80eaf0e20a31942ae5d0728849862446512769692474be5e6b73123a23b"}, {file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17"},
{file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73390ed838f03764540a7bdc4071fe0123914c2cc02fb6abf35182d5fd1b7a42"}, {file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b"},
{file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a22bba012a0c94ec02a7768953020ab0d3e2b884760f859176343a36c01adf87"}, {file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7"},
{file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5385bbfdbc90ff5b2635b7e6bebf259652db00a92b5e3c45b616df75b9058e88"}, {file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a"},
{file = "orjson-3.10.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:02c6279016346e774dd92625d46c6c40db687b8a0d685aadb91e26e46cc33e1e"}, {file = "orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665"},
{file = "orjson-3.10.16-cp312-cp312-win32.whl", hash = "sha256:7ca55097a11426db80f79378e873a8c51f4dde9ffc22de44850f9696b7eb0e8c"}, {file = "orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa"},
{file = "orjson-3.10.16-cp312-cp312-win_amd64.whl", hash = "sha256:86d127efdd3f9bf5f04809b70faca1e6836556ea3cc46e662b44dab3fe71f3d6"}, {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.16-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:148a97f7de811ba14bc6dbc4a433e0341ffd2cc285065199fb5f6a98013744bd"}, {file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a"},
{file = "orjson-3.10.16-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1d960c1bf0e734ea36d0adc880076de3846aaec45ffad29b78c7f1b7962516b8"}, {file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9"},
{file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a318cd184d1269f68634464b12871386808dc8b7c27de8565234d25975a7a137"}, {file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0"},
{file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df23f8df3ef9223d1d6748bea63fca55aae7da30a875700809c500a05975522b"}, {file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307"},
{file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94dda8dd6d1378f1037d7f3f6b21db769ef911c4567cbaa962bb6dc5021cf90"}, {file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e"},
{file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f12970a26666a8775346003fd94347d03ccb98ab8aa063036818381acf5f523e"}, {file = "orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7"},
{file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a1431a245d856bd56e4d29ea0023eb4d2c8f71efe914beb3dee8ab3f0cd7fb"}, {file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8"},
{file = "orjson-3.10.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83655cfc247f399a222567d146524674a7b217af7ef8289c0ff53cfe8db09f0"}, {file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca"},
{file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa59ae64cb6ddde8f09bdbf7baf933c4cd05734ad84dcf4e43b887eb24e37652"}, {file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561"},
{file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ca5426e5aacc2e9507d341bc169d8af9c3cbe88f4cd4c1cf2f87e8564730eb56"}, {file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825"},
{file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6fd5da4edf98a400946cd3a195680de56f1e7575109b9acb9493331047157430"}, {file = "orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890"},
{file = "orjson-3.10.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:980ecc7a53e567169282a5e0ff078393bac78320d44238da4e246d71a4e0e8f5"}, {file = "orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf"},
{file = "orjson-3.10.16-cp313-cp313-win32.whl", hash = "sha256:28f79944dd006ac540a6465ebd5f8f45dfdf0948ff998eac7a908275b4c1add6"}, {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.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7"}, {file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9a18c500f19273e9e104cca8c1f0b40a6470bcccfc33afcc088045d0bf5ea6"},
{file = "orjson-3.10.16-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c35b5c1fb5a5d6d2fea825dec5d3d16bea3c06ac744708a8e1ff41d4ba10cdf1"}, {file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb00b7bfbdf5d34a13180e4805d76b4567025da19a197645ca746fc2fb536586"},
{file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9aac7ecc86218b4b3048c768f227a9452287001d7548500150bb75ee21bf55d"}, {file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33aedc3d903378e257047fee506f11e0833146ca3e57a1a1fb0ddb789876c1e1"},
{file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6e19f5102fff36f923b6dfdb3236ec710b649da975ed57c29833cb910c5a73ab"}, {file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0099ae6aed5eb1fc84c9eb72b95505a3df4267e6962eb93cdd5af03be71c98"},
{file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17210490408eb62755a334a6f20ed17c39f27b4f45d89a38cd144cd458eba80b"}, {file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c864a80a2d467d7786274fce0e4f93ef2a7ca4ff31f7fc5634225aaa4e9e98c"},
{file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbbe04451db85916e52a9f720bd89bf41f803cf63b038595674691680cbebd1b"}, {file = "orjson-3.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c25774c9e88a3e0013d7d1a6c8056926b607a61edd423b50eb5c88fd7f2823ae"},
{file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a966eba501a3a1f309f5a6af32ed9eb8f316fa19d9947bac3e6350dc63a6f0a"}, {file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e78c211d0074e783d824ce7bb85bf459f93a233eb67a5b5003498232ddfb0e8a"},
{file = "orjson-3.10.16-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01e0d22f06c81e6c435723343e1eefc710e0510a35d897856766d475f2a15687"}, {file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:43e17289ffdbbac8f39243916c893d2ae41a2ea1a9cbb060a56a4d75286351ae"},
{file = "orjson-3.10.16-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7c1e602d028ee285dbd300fb9820b342b937df64d5a3336e1618b354e95a2569"}, {file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:781d54657063f361e89714293c095f506c533582ee40a426cb6489c48a637b81"},
{file = "orjson-3.10.16-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d230e5020666a6725629df81e210dc11c3eae7d52fe909a7157b3875238484f3"}, {file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6875210307d36c94873f553786a808af2788e362bd0cf4c8e66d976791e7b528"},
{file = "orjson-3.10.16-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0f8baac07d4555f57d44746a7d80fbe6b2c4fe2ed68136b4abb51cfec512a5e9"}, {file = "orjson-3.10.15-cp38-cp38-win32.whl", hash = "sha256:305b38b2b8f8083cc3d618927d7f424349afce5975b316d33075ef0f73576b60"},
{file = "orjson-3.10.16-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:524e48420b90fc66953e91b660b3d05faaf921277d6707e328fde1c218b31250"}, {file = "orjson-3.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:5dd9ef1639878cc3efffed349543cbf9372bdbd79f478615a1c633fe4e4180d1"},
{file = "orjson-3.10.16-cp39-cp39-win32.whl", hash = "sha256:a9f614e31423d7292dbca966a53b2d775c64528c7d91424ab2747d8ab8ce5c72"}, {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.16-cp39-cp39-win_amd64.whl", hash = "sha256:c338dc2296d1ed0d5c5c27dfb22d00b330555cb706c2e0be1e1c3940a0895905"}, {file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d433bf32a363823863a96561a555227c18a522a8217a6f9400f00ddc70139ae2"},
{file = "orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10"}, {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]] [[package]]
@@ -2751,13 +2699,13 @@ testing = ["pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "pre-commit" name = "pre-commit"
version = "4.2.0" version = "4.1.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks." description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"},
{file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"},
] ]
[package.dependencies] [package.dependencies]
@@ -3154,26 +3102,6 @@ files = [
[package.dependencies] [package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pydantic-settings"
version = "2.8.1"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c"},
{file = "pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585"},
]
[package.dependencies]
pydantic = ">=2.7.0"
python-dotenv = ">=0.21.0"
[package.extras]
azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
toml = ["tomli (>=2.0.1)"]
yaml = ["pyyaml (>=6.0.1)"]
[[package]] [[package]]
name = "pyfiglet" name = "pyfiglet"
version = "1.0.2" version = "1.0.2"
@@ -3233,13 +3161,13 @@ diagrams = ["jinja2", "railroad-diagrams"]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.3.5" version = "8.3.4"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
] ]
[package.dependencies] [package.dependencies]
@@ -3304,26 +3232,6 @@ pytest = ">=6.2.5"
[package.extras] [package.extras]
dev = ["pre-commit", "pytest-asyncio", "tox"] 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]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@@ -3338,20 +3246,6 @@ files = [
[package.dependencies] [package.dependencies]
six = ">=1.5" six = ">=1.5"
[[package]]
name = "python-dotenv"
version = "1.1.0"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.9"
files = [
{file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"},
{file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.20" version = "0.0.20"
@@ -3613,6 +3507,109 @@ files = [
attrs = ">=22.2.0" attrs = ">=22.2.0"
rpds-py = ">=0.7.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]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.32.3"
@@ -3981,25 +3978,6 @@ files = [
{file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"},
] ]
[[package]]
name = "sse-starlette"
version = "2.1.3"
description = "SSE plugin for Starlette"
optional = false
python-versions = ">=3.8"
files = [
{file = "sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772"},
{file = "sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169"},
]
[package.dependencies]
anyio = "*"
starlette = "*"
uvicorn = "*"
[package.extras]
examples = ["fastapi"]
[[package]] [[package]]
name = "stack-data" name = "stack-data"
version = "0.6.3" version = "0.6.3"
@@ -4052,13 +4030,13 @@ widechars = ["wcwidth"]
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "2.5.0" version = "2.4.0"
description = "ANSI color formatting for output in terminal" description = "ANSI color formatting for output in terminal"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.8"
files = [ files = [
{file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"},
{file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"},
] ]
[package.extras] [package.extras]
@@ -4560,4 +4538,4 @@ propcache = ">=0.2.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "f8777299520e96d3c5d14f5a216b36ed869a6429434f1366761cf1cdfe07ef52" content-hash = "28a2b74bfafa9f93d14d2f8d1fcaffa340db212acce6469d6714d342203ad77f"
+3 -11
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "agentic_security" name = "agentic_security"
version = "0.7.2" version = "0.5.1"
description = "Agentic LLM vulnerability scanner" description = "Agentic LLM vulnerability scanner"
authors = ["Alexander Miasoiedov <msoedov@gmail.com>"] authors = ["Alexander Miasoiedov <msoedov@gmail.com>"]
maintainers = ["Alexander Miasoiedov <msoedov@gmail.com>"] maintainers = ["Alexander Miasoiedov <msoedov@gmail.com>"]
@@ -52,10 +52,8 @@ sentry_sdk = "^2.22.0"
orjson = "^3.10" orjson = "^3.10"
pyfiglet = "^1.0.2" pyfiglet = "^1.0.2"
termcolor = "^2.4.0" termcolor = "^2.4.0"
mcp = "^1.4.1"
# garak = { version = "*", optional = true } # garak = { version = "*", optional = true }
pytest-xdist = "3.6.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
@@ -70,7 +68,7 @@ pytest-mock = "^3.14.0"
black = ">=24.10,<26.0" black = ">=24.10,<26.0"
mypy = "^1.12.0" mypy = "^1.12.0"
pre-commit = "^4.0.1" pre-commit = "^4.0.1"
huggingface-hub = ">=0.25.1,<0.30.0" huggingface-hub = ">=0.25.1,<0.29.0"
# Docs # Docs
mkdocs = ">=1.4.2" mkdocs = ">=1.4.2"
@@ -88,13 +86,7 @@ build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "--durations=5 -m 'not slow' -n 3" addopts = "--durations=5 -m 'not slow'"
asyncio_mode = "auto" asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function" asyncio_default_fixture_loop_scope = "function"
markers = "slow: marks tests as slow" markers = "slow: marks tests as slow"
[project]
# MCP requires the following fields to be present in the pyproject.toml file
name = "agentic_security"
version = "1.0.0"
requires-python = ">=3.11"
-9
View File
@@ -1,17 +1,8 @@
import os import os
import pytest import pytest
from cache_to_disk import delete_old_disk_caches
from agentic_security.logutils import logger
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
if "slow" in item.keywords and not os.getenv("RUN_SLOW_TESTS"): if "slow" in item.keywords and not os.getenv("RUN_SLOW_TESTS"):
pytest.skip("Skipping slow test") pytest.skip("Skipping slow test")
@pytest.fixture(autouse=True, scope="session")
def setup_delete_old_disk_caches():
logger.info("delete_old_disk_caches")
delete_old_disk_caches()
View File
+161
View File
@@ -0,0 +1,161 @@
import pytest
import asyncio
from fastapi import FastAPI
from asyncio import Queue, Event
from agentic_security.core.app import create_app, get_tools_inbox, get_stop_event, get_current_run, set_current_run
class TestApp:
"""Test suite for agentic_security.core.app module."""
def test_create_app(self):
"""Test that create_app returns a FastAPI instance."""
app = create_app()
assert isinstance(app, FastAPI)
@pytest.mark.asyncio
async def test_get_tools_inbox(self):
"""Test that get_tools_inbox returns the global Queue instance."""
queue1 = get_tools_inbox()
await queue1.put("test item")
queue2 = get_tools_inbox()
result = queue2.get_nowait()
assert result == "test item"
def test_get_stop_event(self):
"""Test that get_stop_event returns the global Event instance and is not set initially."""
event = get_stop_event()
assert isinstance(event, Event)
assert not event.is_set()
def test_current_run_initial(self):
"""Test that get_current_run returns the global current_run with default values initially."""
run = get_current_run()
# Default values should be empty strings
assert run["spec"] == ""
assert run["id"] == ""
def test_set_current_run(self):
"""Test that set_current_run correctly updates current_run."""
spec = "test run"
result = set_current_run(spec)
expected_id = hash(id(spec))
# Verify that spec is set correctly
assert result["spec"] == spec
assert result["id"] == expected_id
def test_current_run_after_set(self):
"""Test that get_current_run returns the updated current_run after set_current_run is called."""
spec = "another test run"
set_current_run(spec)
current = get_current_run()
assert current["spec"] == spec
assert current["id"] == hash(id(spec))
def test_tools_inbox_same_instance(self):
"""Test that get_tools_inbox returns the same Queue instance by default."""
queue1 = get_tools_inbox()
queue2 = get_tools_inbox()
assert queue1 is queue2
def test_stop_event_set(self):
"""Test that setting the stop event is reflected in subsequent calls."""
event = get_stop_event()
event.set() # set the global event
# Now, subsequent calls should return the same event which is set.
event2 = get_stop_event()
assert event2.is_set()
def test_set_current_run_with_none(self):
"""Test that set_current_run handles None as a valid input and updates current_run accordingly."""
result = set_current_run(None)
expected_id = hash(id(None))
assert result["spec"] is None
assert result["id"] == expected_id
def test_multiple_current_run_assignments(self):
"""Test multiple assignments to current_run to ensure it always updates correctly."""
first_spec = "first run"
result1 = set_current_run(first_spec)
expected_id1 = hash(id(first_spec))
assert result1["spec"] == first_spec
assert result1["id"] == expected_id1
second_spec = "second run"
result2 = set_current_run(second_spec)
expected_id2 = hash(id(second_spec))
assert result2["spec"] == second_spec
assert result2["id"] == expected_id2
current = get_current_run()
# The current_run should reflect the latest assignment.
assert current["spec"] == second_spec
assert current["id"] == expected_id2
@pytest.mark.asyncio
async def test_empty_tools_inbox_exception(self):
"""Test that calling get_nowait on an empty tools_inbox raises QueueEmpty."""
from asyncio import QueueEmpty
queue = get_tools_inbox()
# Clear any existing items in the queue
while True:
try:
queue.get_nowait()
except QueueEmpty:
break
with pytest.raises(QueueEmpty):
queue.get_nowait()
def test_set_current_run_with_dict(self):
"""Test that set_current_run correctly handles a dictionary input as spec."""
spec = {"key": "value"}
result = set_current_run(spec)
expected_id = hash(id(spec))
assert result["spec"] == spec
assert result["id"] == expected_id
@pytest.mark.asyncio
async def test_stop_event_wait(self):
"""Test that waiting on the stop event returns once the event is set."""
event = get_stop_event()
event.clear() # ensure event is not set
async def waiter():
await event.wait()
return True
waiter_task = asyncio.create_task(waiter())
# Wait a moment to ensure the waiter is pending
await asyncio.sleep(0.1)
assert not waiter_task.done()
event.set()
result = await waiter_task
assert result is True
def test_set_current_run_with_int(self):
"""Test that set_current_run handles an integer input as spec."""
spec = 12345
result = set_current_run(spec)
expected_id = hash(id(spec))
assert result["spec"] == spec
assert result["id"] == expected_id
def test_create_app_routes(self):
"""Test that create_app returns a FastAPI instance with default routes available."""
app = create_app()
paths = [route.path for route in app.routes]
# Check that the default OpenAPI route exists
assert "/openapi.json" in paths
@pytest.mark.asyncio
async def test_tools_inbox_async_put_get_order(self):
"""Test that tools_inbox preserves order when items are added and retrieved asynchronously."""
queue = get_tools_inbox()
# Clear any existing items in the queue
from asyncio import QueueEmpty
while True:
try:
queue.get_nowait()
except QueueEmpty:
break
items = ["first", "second", "third"]
for item in items:
await queue.put(item)
result_items = []
for _ in items:
result_items.append(await queue.get())
assert result_items == items
+341
View File
@@ -0,0 +1,341 @@
import pytest
import base64
import httpx
import asyncio
from agentic_security.http_spec import (
LLMSpec,
parse_http_spec,
escape_special_chars_for_json,
encode_image_base64_by_url,
encode_audio_base64_by_url,
InvalidHTTPSpecError,
Modality
)
################################################################################
# Tests for agentic_security/http_spec.py
################################################################################
def test_escape_special_chars_for_json():
"""Test escaping special characters in a prompt for JSON safety."""
prompt = 'Line1\nLine2\t"Quote"\\Backslash'
escaped = escape_special_chars_for_json(prompt)
assert '\\n' in escaped
assert '\\t' in escaped
assert '\\"' in escaped
assert '\\\\' in escaped
def test_parse_http_spec_text():
"""Test parsing a text HTTP spec without image/audio/files requirements."""
spec = "POST http://example.com/api\nContent-Type: application/json\n\nThis is a prompt: <<PROMPT>>"
llm_spec = parse_http_spec(spec)
assert llm_spec.method == "POST"
assert llm_spec.url == "http://example.com/api"
assert llm_spec.headers["Content-Type"] == "application/json"
assert "<<PROMPT>>" in llm_spec.body
assert not llm_spec.has_files
assert not llm_spec.has_image
assert not llm_spec.has_audio
def test_parse_http_spec_files():
"""Test parsing a HTTP spec with multipart/form-data header indicating files."""
spec = "PUT http://example.com/upload\nContent-Type: multipart/form-data\n\nFile upload test"
llm_spec = parse_http_spec(spec)
assert llm_spec.has_files
def test_parse_http_spec_image_audio():
"""Test parsing a HTTP spec that requires image and audio via placeholders."""
spec = "GET http://example.com/api\nContent-Type: application/json\n\nImage: <<BASE64_IMAGE>> and Audio: <<BASE64_AUDIO>>"
llm_spec = parse_http_spec(spec)
assert llm_spec.has_image
assert llm_spec.has_audio
def test_encode_image_base64_by_url(monkeypatch):
"""Test that image encoding returns the correct base64 string with prefix."""
dummy_content = b'test_image'
class DummyResponse:
def __init__(self, content):
self.content = content
def dummy_get(url):
return DummyResponse(dummy_content)
monkeypatch.setattr(httpx, "get", dummy_get)
result = encode_image_base64_by_url("http://dummyurl.com/image.jpg")
expected = "data:image/jpeg;base64," + base64.b64encode(dummy_content).decode("utf-8")
assert result == expected
def test_encode_audio_base64_by_url(monkeypatch):
"""Test that audio encoding returns the correct base64 string with prefix."""
dummy_content = b'test_audio'
class DummyResponse:
def __init__(self, content):
self.content = content
def dummy_get(url):
return DummyResponse(dummy_content)
monkeypatch.setattr(httpx, "get", dummy_get)
result = encode_audio_base64_by_url("http://dummyurl.com/audio.mp3")
expected = "data:audio/mpeg;base64," + base64.b64encode(dummy_content).decode("utf-8")
assert result == expected
@pytest.mark.asyncio
async def test_probe_text(monkeypatch):
"""Test the probe function for text modality by replacing <<PROMPT>>."""
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"prompt\": \"<<PROMPT>>\"}"
llm_spec = parse_http_spec(spec)
async def dummy_request(self, method, url, headers, content, timeout):
return httpx.Response(200, text="ok")
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
response = await llm_spec.probe("Hello")
assert response.status_code == 200
assert "ok" in response.text
@pytest.mark.asyncio
async def test_probe_with_files(monkeypatch):
"""Test that probe correctly branches to _probe_with_files when files are provided."""
spec = "POST http://example.com/api\nContent-Type: multipart/form-data\n\nFile data"
llm_spec = parse_http_spec(spec)
files = {"file": ("dummy.txt", b"data")}
async def dummy_request(self, method, url, headers, files, timeout):
return httpx.Response(200, text="file upload ok")
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
response = await llm_spec.probe("Unused", files=files)
assert response.status_code == 200
assert "file upload ok" in response.text
@pytest.mark.asyncio
async def test_verify_image(monkeypatch):
"""Test verify method branch for image modality by monkeypatching image encoder."""
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"image\": \"<<BASE64_IMAGE>>\"}"
llm_spec = parse_http_spec(spec)
# Replace the image encoder to return a dummy string
monkeypatch.setattr("agentic_security.http_spec.encode_image_base64_by_url", lambda url="": "dummy_image")
async def dummy_request(self, method, url, headers, content, timeout):
# Check that the dummy image is injected in the content
assert "dummy_image" in content
return httpx.Response(200, text="image ok")
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
response = await llm_spec.verify()
assert response.status_code == 200
assert "image ok" in response.text
@pytest.mark.asyncio
async def test_verify_audio(monkeypatch):
"""Test verify method branch for audio modality by monkeypatching audio encoder."""
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"audio\": \"<<BASE64_AUDIO>>\"}"
llm_spec = parse_http_spec(spec)
monkeypatch.setattr("agentic_security.http_spec.encode_audio_base64_by_url", lambda url: "dummy_audio")
async def dummy_request(self, method, url, headers, content, timeout):
# Ensure that the dummy audio string is present in the request content
assert "dummy_audio" in content
return httpx.Response(200, text="audio ok")
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
response = await llm_spec.verify()
assert response.status_code == 200
assert "audio ok" in response.text
@pytest.mark.asyncio
async def test_verify_files(monkeypatch):
"""Test verify method branch for files modality where _probe_with_files is invoked."""
spec = "POST http://example.com/api\nContent-Type: multipart/form-data\n\nFile data"
llm_spec = parse_http_spec(spec)
async def dummy_request(self, method, url, headers, files, timeout):
return httpx.Response(200, text="files ok")
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
response = await llm_spec.verify()
assert response.status_code == 200
assert "files ok" in response.text
def test_llm_spec_modality_property():
"""Test that the modality property reflects the correct modality."""
spec_text = "POST http://example.com/api\nContent-Type: application/json\n\nPrompt: <<PROMPT>>"
llm_spec_text = parse_http_spec(spec_text)
assert llm_spec_text.modality == Modality.TEXT
spec_image = "POST http://example.com/api\nContent-Type: application/json\n\nImage: <<BASE64_IMAGE>>"
llm_spec_image = parse_http_spec(spec_image)
assert llm_spec_image.modality == Modality.IMAGE
spec_audio = "POST http://example.com/api\nContent-Type: application/json\n\nAudio: <<BASE64_AUDIO>>"
llm_spec_audio = parse_http_spec(spec_audio)
assert llm_spec_audio.modality == Modality.AUDIO
def test_from_string_invalid():
"""Test that LLMSpec.from_string raises an error for an invalid spec."""
invalid_spec = "INVALID_SPEC"
with pytest.raises(InvalidHTTPSpecError):
LLMSpec.from_string(invalid_spec)
@pytest.mark.asyncio
async def test_validate_missing_files():
"""Test that LLMSpec.validate raises a ValueError when files are required but missing."""
spec = "POST http://example.com/api\nContent-Type: multipart/form-data\n\nFile upload test"
llm_spec = parse_http_spec(spec)
with pytest.raises(ValueError, match="Files are required"):
llm_spec.validate("test prompt", "", "", {})
@pytest.mark.asyncio
async def test_validate_missing_image():
"""Test that LLMSpec.validate raises a ValueError when an image is required but missing."""
spec = "POST http://example.com/api\nContent-Type: application/json\n\nImage: <<BASE64_IMAGE>>"
llm_spec = parse_http_spec(spec)
with pytest.raises(ValueError, match="An image is required"):
llm_spec.validate("test prompt", "", "dummy_audio", {})
@pytest.mark.asyncio
async def test_validate_missing_audio():
"""Test that LLMSpec.validate raises a ValueError when audio is required but missing."""
spec = "POST http://example.com/api\nContent-Type: application/json\n\nAudio: <<BASE64_AUDIO>>"
llm_spec = parse_http_spec(spec)
with pytest.raises(ValueError, match="Audio is required"):
llm_spec.validate("test prompt", "dummy_image", "", {})
def test_fn_alias(monkeypatch):
"""Test that LLMSpec.fn is a functional alias for LLMSpec.probe."""
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"prompt\": \"<<PROMPT>>\"}"
llm_spec = parse_http_spec(spec)
# Instead of overriding the instance method, verify the alias at the class level.
assert LLMSpec.fn is LLMSpec.probe
def test_escape_special_chars_no_special():
"""Test that the escape function returns the original string if no special characters are present."""
prompt = "Simple text without specials"
escaped = escape_special_chars_for_json(prompt)
assert escaped == "Simple text without specials"
@pytest.mark.asyncio
async def test_probe_text_with_special_chars(monkeypatch):
"""Test probe for text modality with special characters in prompt ensuring escaped content."""
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"prompt\": \"<<PROMPT>>\"}"
llm_spec = parse_http_spec(spec)
captured = {}
async def dummy_request(self, method, url, headers, content, timeout):
captured['content'] = content
return httpx.Response(200, text="ok")
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
test_prompt = 'Hello\nWorld\t"Test"'
response = await llm_spec.probe(test_prompt)
expected_escaped = escape_special_chars_for_json(test_prompt)
assert expected_escaped in captured['content']
assert response.status_code == 200
@pytest.mark.asyncio
async def test_verify_both_image_audio(monkeypatch):
"""Test verify method when both image and audio placeholders are present.
Expect a ValueError because only the image branch is triggered by pattern matching and the missing audio causes validation to fail."""
spec = ("POST http://example.com/api\nContent-Type: application/json\n\n"
"{\"audio\": \"<<BASE64_AUDIO>>\", \"image\":\"<<BASE64_IMAGE>>\"}")
llm_spec = parse_http_spec(spec)
# Monkey patch the image encoder to return a dummy value
monkeypatch.setattr("agentic_security.http_spec.encode_image_base64_by_url", lambda url="": "dummy_image")
with pytest.raises(ValueError, match="Audio is required"):
await llm_spec.verify()
def test_parse_http_spec_invalid_header_format():
"""Test that parse_http_spec raises an error when a header line doesn't have the expected 'key: value' format."""
invalid_spec = "GET http://example.com/api\nInvalidHeaderWithoutColon\n\nBody with <<PROMPT>>"
with pytest.raises(ValueError):
parse_http_spec(invalid_spec)
def test_from_string_valid():
"""Test that LLMSpec.from_string returns a valid LLMSpec object when given a proper spec string."""
spec = "GET http://example.com/api\nContent-Type: application/json\n\n{ \"prompt\": \"<<PROMPT>>\" }"
llm_spec = LLMSpec.from_string(spec)
assert llm_spec.method == "GET"
assert llm_spec.url == "http://example.com/api"
@pytest.mark.asyncio
async def test_parse_http_spec_multiline_body():
"""Test parsing an HTTP spec with a multiline body to ensure body concatenation works."""
spec = (
"PATCH http://example.com/api\n"
"Content-Type: application/json\n"
"\n"
"Line one of body\n"
"Line two of body\n"
"Line three"
)
llm_spec = parse_http_spec(spec)
# As implemented, the parser concatenates lines without newline delimiters
expected_body = "Line one of bodyLine two of bodyLine three"
assert llm_spec.body == expected_body
@pytest.mark.asyncio
async def test_encode_image_default_argument(monkeypatch):
"""Test that encode_image_base64_by_url works with its default URL argument."""
dummy_content = b'default_image'
class DummyResponse:
def __init__(self, content):
self.content = content
def dummy_get(url):
# check that the default URL (which includes 'fluidicon.png') is used
assert "fluidicon.png" in url
return DummyResponse(dummy_content)
monkeypatch.setattr(httpx, "get", dummy_get)
result = encode_image_base64_by_url()
expected = "data:image/jpeg;base64," + base64.b64encode(dummy_content).decode("utf-8")
assert result == expected
@pytest.mark.asyncio
async def test_probe_without_prompt_placeholder(monkeypatch):
"""Test the probe function when the request body does not include the <<PROMPT>> placeholder."""
spec = "POST http://example.com/api\nContent-Type: application/json\n\n{\"message\": \"No placeholder here\"}"
llm_spec = parse_http_spec(spec)
captured = {}
async def dummy_request(self, method, url, headers, content, timeout):
captured['content'] = content
return httpx.Response(200, text="ok without placeholder")
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
response = await llm_spec.probe("Ignored prompt")
assert "No placeholder here" in captured['content']
assert response.status_code == 200
def test_validate_success():
"""Test that LLMSpec.validate does not raise an error when all required data is provided."""
# Test case for files: files are provided as required
spec_files = "POST http://example.com/api\nContent-Type: multipart/form-data\n\nFile upload"
llm_spec_files = parse_http_spec(spec_files)
llm_spec_files.validate("some prompt", "dummy_image", "dummy_audio", {"file": ("dummy.txt", b"data")})
# Test case for image: image is provided as required
spec_image = "POST http://example.com/api\nContent-Type: application/json\n\nImage: <<BASE64_IMAGE>>"
llm_spec_image = parse_http_spec(spec_image)
llm_spec_image.validate("some prompt", "dummy_image", "dummy_audio", {})
# Test case for audio: audio is provided as required
spec_audio = "POST http://example.com/api\nContent-Type: application/json\n\nAudio: <<BASE64_AUDIO>>"
llm_spec_audio = parse_http_spec(spec_audio)
llm_spec_audio.validate("some prompt", "dummy_image", "dummy_audio", {})
@pytest.mark.asyncio
async def test_probe_invalid_url(monkeypatch):
"""Test that probe raises an exception when the HTTP client fails due to an invalid URL."""
spec = "GET http://nonexistent_url/api\nContent-Type: application/json\n\n{\"prompt\": \"<<PROMPT>>\"}"
llm_spec = parse_http_spec(spec)
async def dummy_request(self, method, url, headers, content, timeout):
raise httpx.RequestError("Invalid URL")
monkeypatch.setattr(httpx.AsyncClient, "request", dummy_request)
with pytest.raises(httpx.RequestError):
await llm_spec.probe("Test")
-12
View File
@@ -1,12 +0,0 @@
import pytest
from agentic_security.mcp.client import run
@pytest.mark.asyncio
async def test_mcp_echo_tool():
"""Test the echo tool functionality"""
prompts, resources, tools = await run()
assert prompts
assert resources
assert tools