diff --git a/.github/scripts/detect_changed_packages.py b/.github/scripts/detect_changed_packages.py index 2888ebc2e..9110ca87f 100644 --- a/.github/scripts/detect_changed_packages.py +++ b/.github/scripts/detect_changed_packages.py @@ -18,6 +18,7 @@ # handled separately via labeler.yml auto-labels. DEPENDENTS: dict[str, list[str]] = { "uipath-core": ["uipath-platform", "uipath"], + "uipath-eval": ["uipath"], "uipath-platform": ["uipath"], } diff --git a/.github/workflows/lint-packages.yml b/.github/workflows/lint-packages.yml index d530e25a9..3d688b06b 100644 --- a/.github/workflows/lint-packages.yml +++ b/.github/workflows/lint-packages.yml @@ -198,9 +198,63 @@ jobs: working-directory: packages/uipath run: uv run python scripts/lint_httpx_client.py + lint-uipath-eval: + name: Lint uipath-eval + needs: detect-changed-packages + runs-on: ubuntu-latest + steps: + - name: Check if package changed + id: check + run: | + if echo '${{ needs.detect-changed-packages.outputs.packages }}' | jq -e 'index("uipath-eval")' > /dev/null; then + echo "skip=false" >> $GITHUB_OUTPUT + else + echo "skip=true" >> $GITHUB_OUTPUT + fi + + - name: Skip + if: steps.check.outputs.skip == 'true' + run: echo "Skipping - no changes to uipath-eval" + + - name: Checkout + if: steps.check.outputs.skip != 'true' + uses: actions/checkout@v4 + + - name: Setup uv + if: steps.check.outputs.skip != 'true' + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Setup Python + if: steps.check.outputs.skip != 'true' + uses: actions/setup-python@v5 + with: + python-version-file: "packages/uipath-eval/.python-version" + + - name: Install dependencies + if: steps.check.outputs.skip != 'true' + working-directory: packages/uipath-eval + run: uv sync --locked --no-sources --all-extras + + - name: Check static types + if: steps.check.outputs.skip != 'true' + working-directory: packages/uipath-eval + run: uv run mypy --config-file pyproject.toml . + + - name: Check linting + if: steps.check.outputs.skip != 'true' + working-directory: packages/uipath-eval + run: uv run ruff check . + + - name: Check formatting + if: steps.check.outputs.skip != 'true' + working-directory: packages/uipath-eval + run: uv run ruff format --check . + lint-gate: name: Lint - needs: [lint-uipath-core, lint-uipath-platform, lint-uipath] + needs: [lint-uipath-core, lint-uipath-platform, lint-uipath, lint-uipath-eval] runs-on: ubuntu-latest if: always() steps: @@ -208,7 +262,8 @@ jobs: run: | if [[ "${{ needs.lint-uipath-core.result }}" == "failure" || \ "${{ needs.lint-uipath-platform.result }}" == "failure" || \ - "${{ needs.lint-uipath.result }}" == "failure" ]]; then + "${{ needs.lint-uipath.result }}" == "failure" || \ + "${{ needs.lint-uipath-eval.result }}" == "failure" ]]; then echo "Lint failed" exit 1 fi diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index 3c438f75c..83ceb736f 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -224,4 +224,4 @@ jobs: - name: Publish run: uv publish --index testpypi env: - UV_PUBLISH_TOKEN: ${{ matrix.package == 'uipath' && secrets.TESTPYPI_TOKEN || matrix.package == 'uipath-platform' && secrets.TESTPYPI_TOKEN_PLATFORM || secrets.TESTPYPI_TOKEN_CORE }} + UV_PUBLISH_TOKEN: ${{ matrix.package == 'uipath' && secrets.TESTPYPI_TOKEN || matrix.package == 'uipath-platform' && secrets.TESTPYPI_TOKEN_PLATFORM || matrix.package == 'uipath-eval' && secrets.TESTPYPI_TOKEN_EVAL || secrets.TESTPYPI_TOKEN_CORE }} diff --git a/.github/workflows/test-packages.yml b/.github/workflows/test-packages.yml index dab4b8d7c..84fdba7c5 100644 --- a/.github/workflows/test-packages.yml +++ b/.github/workflows/test-packages.yml @@ -229,11 +229,58 @@ jobs: working-directory: packages/uipath run: uv run pytest - continue-on-error: true + test-uipath-eval: + name: Test (uipath-eval, ${{ matrix.python-version }}, ${{ matrix.os }}) + needs: detect-changed-packages + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12", "3.13"] + os: [ubuntu-latest, windows-latest] + steps: + - name: Check if package changed + id: check + shell: bash + run: | + if echo '${{ needs.detect-changed-packages.outputs.packages }}' | jq -e 'index("uipath-eval")' > /dev/null; then + echo "skip=false" >> $GITHUB_OUTPUT + else + echo "skip=true" >> $GITHUB_OUTPUT + fi + + - name: Skip + if: steps.check.outputs.skip == 'true' + shell: bash + run: echo "Skipping - no changes to uipath-eval" + + - name: Checkout + if: steps.check.outputs.skip != 'true' + uses: actions/checkout@v4 + + - name: Setup uv + if: steps.check.outputs.skip != 'true' + uses: astral-sh/setup-uv@v5 + + - name: Setup Python + if: steps.check.outputs.skip != 'true' + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + if: steps.check.outputs.skip != 'true' + working-directory: packages/uipath-eval + run: uv sync --all-extras --python ${{ matrix.python-version }} + + - name: Run tests + if: steps.check.outputs.skip != 'true' + working-directory: packages/uipath-eval + run: uv run pytest test-gate: name: Test - needs: [test-uipath-core, test-uipath-platform, test-uipath, e2e-uipath-platform] + needs: [test-uipath-core, test-uipath-platform, test-uipath, test-uipath-eval, e2e-uipath-platform] runs-on: ubuntu-latest if: always() steps: @@ -241,7 +288,8 @@ jobs: run: | if [[ "${{ needs.test-uipath-core.result }}" == "failure" || \ "${{ needs.test-uipath-platform.result }}" == "failure" || \ - "${{ needs.test-uipath.result }}" == "failure" ]]; then + "${{ needs.test-uipath.result }}" == "failure" || \ + "${{ needs.test-uipath-eval.result }}" == "failure" ]]; then echo "Tests failed" exit 1 fi diff --git a/.github/workflows/test-uipath-langchain.yml b/.github/workflows/test-uipath-langchain.yml index f74135975..0475e020f 100644 --- a/.github/workflows/test-uipath-langchain.yml +++ b/.github/workflows/test-uipath-langchain.yml @@ -30,6 +30,10 @@ jobs: working-directory: packages/uipath-platform run: uv build + - name: Build uipath-eval package + working-directory: packages/uipath-eval + run: uv build + - name: Build uipath package working-directory: packages/uipath run: uv build @@ -78,6 +82,7 @@ jobs: run: | uv add ../wheels/uipath-core/dist/*.whl --dev uv add ../wheels/uipath-platform/dist/*.whl --dev + uv add ../wheels/uipath-eval/dist/*.whl --dev uv add ../wheels/uipath/dist/*.whl --dev - name: Run uipath-langchain tests @@ -152,6 +157,7 @@ jobs: run: | uv add ../wheels/uipath-core/dist/*.whl uv add ../wheels/uipath-platform/dist/*.whl + uv add ../wheels/uipath-eval/dist/*.whl uv add ../wheels/uipath/dist/*.whl - name: Install dependencies diff --git a/.github/workflows/test-uipath-llamaindex.yml b/.github/workflows/test-uipath-llamaindex.yml index fcf8d0fb4..7c4027b37 100644 --- a/.github/workflows/test-uipath-llamaindex.yml +++ b/.github/workflows/test-uipath-llamaindex.yml @@ -30,6 +30,10 @@ jobs: working-directory: packages/uipath-platform run: uv build + - name: Build uipath-eval package + working-directory: packages/uipath-eval + run: uv build + - name: Build uipath package working-directory: packages/uipath run: uv build @@ -78,6 +82,7 @@ jobs: run: | uv add ../../../wheels/uipath-core/dist/*.whl --dev uv add ../../../wheels/uipath-platform/dist/*.whl --dev + uv add ../../../wheels/uipath-eval/dist/*.whl --dev uv add ../../../wheels/uipath/dist/*.whl --dev - name: Run uipath-llamaindex tests @@ -151,6 +156,7 @@ jobs: run: | uv add ../../../wheels/uipath-core/dist/*.whl uv add ../../../wheels/uipath-platform/dist/*.whl + uv add ../../../wheels/uipath-eval/dist/*.whl uv add ../../../wheels/uipath/dist/*.whl - name: Install dependencies diff --git a/packages/uipath-eval/.python-version b/packages/uipath-eval/.python-version new file mode 100644 index 000000000..2c0733315 --- /dev/null +++ b/packages/uipath-eval/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/packages/uipath-eval/README.md b/packages/uipath-eval/README.md new file mode 100644 index 000000000..1d343390d --- /dev/null +++ b/packages/uipath-eval/README.md @@ -0,0 +1,38 @@ +# uipath-eval + +Standalone evaluator logic extracted from the `uipath` SDK. + +Use this package in `python-eval-workers` and other services that need +evaluator logic without the full UiPath SDK overhead. + +## Install + +```bash +pip install uipath-eval +``` + +For LLM-based evaluators (llm-as-judge, trajectory): + +```bash +pip install "uipath-eval[llm]" +``` + +## Usage + +```python +from uipath_eval import ExactMatchEvaluator +from uipath.eval import LLMJudgeOutputEvaluator # LLM evaluators stay in uipath.eval +from uipath_eval.models import EvaluationResult +``` + +## What's here + +- `uipath_eval.evaluators` — all evaluator implementations +- `uipath_eval.models` — evaluation data models +- `uipath_eval.runtime` — pure asyncio/stdlib runtime utilities + +## What's NOT here + +`UiPathEvalRuntime`, `UiPathEvalContext`, and `evaluate()` depend on +`uipath.runtime` and stay in `uipath.eval`. Use `uipath` if you need +the full eval pipeline with runtime integration. diff --git a/packages/uipath-eval/pyproject.toml b/packages/uipath-eval/pyproject.toml new file mode 100644 index 000000000..68ab8f031 --- /dev/null +++ b/packages/uipath-eval/pyproject.toml @@ -0,0 +1,99 @@ +[project] +name = "uipath-eval" +version = "0.1.0" +description = "UiPath evaluator logic as a standalone package — for use in python-eval-workers without the full UiPath SDK." +readme = { file = "README.md", content-type = "text/markdown" } +requires-python = ">=3.11" +dependencies = [ + "uipath-core>=0.5.8, <0.6.0", + "opentelemetry-sdk>=1.39.0, <2.0.0", + "httpx>=0.28.1", + "pydantic>=2.12.5, <3.0.0", +] +classifiers = [ + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +maintainers = [ + { name = "Marius Cosareanu", email = "marius.cosareanu@uipath.com" }, + { name = "Cristian Pufu", email = "cristian.pufu@uipath.com" }, +] + +[project.optional-dependencies] +llm = [ + "langchain-core>=0.3", + "openai>=1.0", +] + +[project.urls] +Homepage = "https://uipath.com" +Repository = "https://github.com/UiPath/uipath-python" +Documentation = "https://uipath.github.io/uipath-python/" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/uipath_eval"] + +[dependency-groups] +dev = [ + "bandit>=1.8.2", + "mypy>=1.14.1", + "ruff>=0.9.4", + "rust-just>=1.39.0", + "pytest>=7.4.0", + "pytest-asyncio>=1.0.0", + "pytest-httpx>=0.35.0", + "pytest-cov>=4.1.0", + "pytest-mock>=3.11.1", + "pre-commit>=4.5.1", +] + +[tool.ruff] +line-length = 88 +indent-width = 4 + +[tool.ruff.lint] +select = ["E", "F", "B", "I", "D"] +ignore = ["D417", "E501"] + +[tool.ruff.lint.per-file-ignores] +"*" = ["E501"] +"tests/**" = ["D"] +"*_test.py" = ["D"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.mypy] +plugins = ["pydantic.mypy"] +mypy_path = "src" +explicit_package_bases = true +namespace_packages = true +follow_imports = "silent" +warn_redundant_casts = true +warn_unused_ignores = true +disallow_any_generics = true +check_untyped_defs = true +no_implicit_reexport = true +disallow_untyped_defs = false + +[[tool.mypy.overrides]] +module = [ + "tests.*", +] +ignore_errors = true + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] diff --git a/packages/uipath-eval/src/uipath_eval/__init__.py b/packages/uipath-eval/src/uipath_eval/__init__.py new file mode 100644 index 000000000..a1a1054d5 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/__init__.py @@ -0,0 +1,82 @@ +"""UiPath evaluator logic — standalone package. + +Provides evaluators, models, and pure runtime utilities without +the full UiPath SDK. Intended for use in python-eval-workers. + +For runtime integration (UiPathEvalRuntime, evaluate), use uipath.eval. +""" + +from .evaluators import ( + EVALUATORS, + BaseEvaluationCriteria, + BaseEvaluator, + BaseEvaluatorConfig, + BaseEvaluatorJustification, + BaseLegacyEvaluator, + BinaryClassificationEvaluator, + ContainsEvaluator, + ExactMatchEvaluator, + JsonSimilarityEvaluator, + LegacyExactMatchEvaluator, + LegacyJsonSimilarityEvaluator, + MulticlassClassificationEvaluator, + ToolCallArgsEvaluator, + ToolCallCountEvaluator, + ToolCallOrderEvaluator, + ToolCallOutputEvaluator, +) +from .models import ( + AgentExecution, + BooleanEvaluationResult, + ErrorEvaluationResult, + EvalItemResult, + EvaluationResult, + EvaluationResultDto, + EvaluatorType, + LegacyEvaluatorCategory, + LegacyEvaluatorType, + LLMResponse, + NumericEvaluationResult, + ScoreType, + ToolCall, + ToolOutput, +) + +__all__ = [ + "EVALUATORS", + # Base classes + "BaseEvaluator", + "BaseEvaluationCriteria", + "BaseEvaluatorConfig", + "BaseEvaluatorJustification", + "BaseLegacyEvaluator", + # Coded evaluators + "BinaryClassificationEvaluator", + "ContainsEvaluator", + "ExactMatchEvaluator", + "JsonSimilarityEvaluator", + "MulticlassClassificationEvaluator", + # Tool call evaluators + "ToolCallArgsEvaluator", + "ToolCallCountEvaluator", + "ToolCallOrderEvaluator", + "ToolCallOutputEvaluator", + # Legacy evaluators (deterministic only — LLM/platform variants stay in uipath) + "LegacyExactMatchEvaluator", + "LegacyJsonSimilarityEvaluator", + # Models + "AgentExecution", + "BooleanEvaluationResult", + "ErrorEvaluationResult", + "EvalItemResult", + "EvaluationResult", + "EvaluationResultDto", + "EvaluatorType", + "LegacyEvaluatorCategory", + "LegacyEvaluatorType", + "LLMResponse", + "NumericEvaluationResult", + "ScoreType", + "ToolCall", + "ToolOutput", +] diff --git a/packages/uipath-eval/src/uipath_eval/_execution_context.py b/packages/uipath-eval/src/uipath_eval/_execution_context.py new file mode 100644 index 000000000..18596fddf --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/_execution_context.py @@ -0,0 +1,44 @@ +"""Execution context variables and span collection shared between eval.runtime and eval.mocks. + +This is a leaf module with no internal dependencies (only opentelemetry), +so it can be safely imported from anywhere without triggering circular imports. +""" + +from collections import defaultdict +from contextvars import ContextVar + +from opentelemetry.sdk.trace import ReadableSpan, Span + + +class ExecutionSpanCollector: + """Collects spans as they are created during execution.""" + + def __init__(self): + # { execution_id -> list of spans } + self._spans: dict[str, list[ReadableSpan]] = defaultdict(list) + + def add_span(self, span: Span, execution_id: str) -> None: + self._spans[execution_id].append(span) + + def get_spans(self, execution_id: str) -> list[ReadableSpan]: + return self._spans.get(execution_id, []) + + def clear(self, execution_id: str | None = None) -> None: + if execution_id: + self._spans.pop(execution_id, None) + else: + self._spans.clear() + + +# Span collector for trace access during mocking +span_collector_context: ContextVar[ExecutionSpanCollector | None] = ContextVar( + "span_collector", default=None +) + +# Execution ID for the current evaluation item +execution_id_context: ContextVar[str | None] = ContextVar("execution_id", default=None) + +# Evaluation set run ID (action ID) for grouping related LLM calls +eval_set_run_id_context: ContextVar[str | None] = ContextVar( + "eval_set_run_id", default=None +) diff --git a/packages/uipath-eval/src/uipath_eval/_helpers/__init__.py b/packages/uipath-eval/src/uipath_eval/_helpers/__init__.py new file mode 100644 index 000000000..81906af6c --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/_helpers/__init__.py @@ -0,0 +1,5 @@ +"""Evaluation helper utilities.""" + +from .helpers import is_empty_value, track_evaluation_metrics + +__all__ = ["is_empty_value", "track_evaluation_metrics"] diff --git a/packages/uipath-eval/src/uipath_eval/_helpers/evaluators_helpers.py b/packages/uipath-eval/src/uipath_eval/_helpers/evaluators_helpers.py new file mode 100644 index 000000000..86c856eda --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/_helpers/evaluators_helpers.py @@ -0,0 +1,521 @@ +import ast +import json +from collections.abc import Mapping, Sequence +from datetime import datetime +from typing import Any + +from opentelemetry.sdk.trace import ReadableSpan + +from ..models import ( + ToolCall, + ToolOutput, +) + +COMPARATOR_MAPPINGS = { + ">": "gt", + "<": "lt", + ">=": "ge", + "<=": "le", + "=": "eq", + "==": "eq", + "!=": "ne", +} + + +def extract_tool_calls_names(spans: Sequence[ReadableSpan]) -> list[str]: + """Extract the tool call names from execution spans IN ORDER. + + Args: + spans: List of ReadableSpan objects from agent execution. + + Returns: + List of tool names in the order they were called. + """ + tool_calls_names = [] + + for span in spans: + # Check for tool.name attribute first + if span.attributes and (tool_name := span.attributes.get("tool.name")): + tool_calls_names.append(str(tool_name)) + + return tool_calls_names + + +def extract_tool_calls(spans: Sequence[ReadableSpan]) -> list[ToolCall]: + """Extract the tool calls from execution spans with their arguments. + + Args: + spans: List of ReadableSpan objects from agent execution. + + Returns: + Dict of tool calls with their arguments. + """ + tool_calls = [] + + for span in spans: + if span.attributes and (tool_name := span.attributes.get("tool.name")): + try: + input_value: Any = span.attributes.get("input.value", {}) + # Ensure input_value is a string before parsing + if isinstance(input_value, str): + arguments = ast.literal_eval(input_value) + elif isinstance(input_value, dict): + arguments = input_value + else: + arguments = {} + tool_calls.append(ToolCall(name=str(tool_name), args=arguments)) + except (SyntaxError, ValueError): + # Handle case where input.value is not valid Python syntax + tool_calls.append(ToolCall(name=str(tool_name), args={})) + + return tool_calls + + +def extract_tool_calls_outputs(spans: Sequence[ReadableSpan]) -> list[ToolOutput]: + """Extract the outputs of the tool calls from execution spans. + + Args: + spans: List of ReadableSpan objects from agent execution. + + Returns: + List of tool calls outputs. + """ + # After span normalization, the output.value should always be a dict with a content field + # We keep this list of potential output keys for extensibility purposes (e.g. frameworks without span normalization) + potential_output_keys = ["content"] + tool_calls_outputs = [] + for span in spans: + if span.attributes and (tool_name := span.attributes.get("tool.name")): + output = span.attributes.get("output.value", "") + final_output = "" + + # Handle different output formats + if isinstance(output, str): + try: + # Try to parse as JSON and extract content field + parsed_output = json.loads(output) + if isinstance(parsed_output, dict): + for key in potential_output_keys: + if key in parsed_output: + final_output = parsed_output[key] + break + else: + # If parsed JSON is not a dict, use the original string + final_output = output + except (json.JSONDecodeError, ValueError): + # If parsing fails, use the string as-is + final_output = output + elif isinstance(output, dict): + # If output is already a dict, extract content field + for key in potential_output_keys: + if key in output: + final_output = output.get(key, "") + break + else: + final_output = str(output) + + tool_calls_outputs.append( + ToolOutput( + name=str(tool_name), + output=str(final_output) if final_output else "", + ) + ) + return tool_calls_outputs + + +def tool_calls_order_score( + actual_tool_calls_names: Sequence[str], + expected_tool_calls_names: Sequence[str], + strict: bool = False, +) -> tuple[float, dict[str, Any]]: + """The function calculates a score based on LCS applied to the order of the tool calls. + + It calculates the longest common subsequence between the actual tool calls + and the expected tool calls and returns the ratio of the LCS length to the number of + expected calls. + + Args: + actual_tool_calls_names: List of tool names in the actual order + expected_tool_calls_names: List of tool names in the expected order + strict: If True, the function will return 0 if the actual calls do not match the expected calls exactly + + Returns: + tuple[float, dict]: Ratio of the LCS length to the number of expected, and the justification dict + """ + justification = { + "actual": str(list(actual_tool_calls_names)), + "expected": str(list(expected_tool_calls_names)), + "lcs": [], + } + + # Handle empty cases + if not expected_tool_calls_names and not actual_tool_calls_names: + return 1.0, justification + elif not expected_tool_calls_names or not actual_tool_calls_names: + return 0.0, justification + + # Handle exact match + if expected_tool_calls_names == actual_tool_calls_names: + justification["lcs"] = list(actual_tool_calls_names) + return 1.0, justification + + # Handle strict mode - only perfect matches allowed + if strict: + return 0.0, justification + + # Calculate LCS with full DP table for efficient reconstruction + m, n = len(actual_tool_calls_names), len(expected_tool_calls_names) + dp = [[0] * (n + 1) for _ in range(m + 1)] + + # Build DP table - O(m*n) + for i in range(1, m + 1): + for j in range(1, n + 1): + if actual_tool_calls_names[i - 1] == expected_tool_calls_names[j - 1]: + dp[i][j] = dp[i - 1][j - 1] + 1 + else: + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + + # Reconstruct LCS - O(m+n) + lcs = [] + i, j = m, n + while i > 0 and j > 0: + if actual_tool_calls_names[i - 1] == expected_tool_calls_names[j - 1]: + lcs.append(actual_tool_calls_names[i - 1]) + i -= 1 + j -= 1 + elif dp[i - 1][j] > dp[i][j - 1]: + i -= 1 + else: + j -= 1 + + lcs.reverse() # Reverse to get correct order + lcs_length = len(lcs) + justification["lcs"] = lcs + return lcs_length / n, justification + + +def tool_calls_count_score( + actual_tool_calls_count: Mapping[str, int], + expected_tool_calls_count: Mapping[str, tuple[str, int]], + strict: bool = False, + justification_key: str = "explained_tool_calls_count", +) -> tuple[float, dict[str, Any]]: + """Check if the expected tool call counts match the actual tool call counts. + + Args: + actual_tool_calls_count: Mapping of tool names to their actual call counts. + expected_tool_calls_count: Mapping of tool names to expected (comparator, count) tuples. + strict: If True, the function will return 0 if not all expected tool calls are matched. + justification_key: Key to use for the justification in the returned dict. + + Returns: + tuple[float, dict]: Score based on the number of matches, and the justification dict. + """ + if not expected_tool_calls_count and not actual_tool_calls_count: + return 1.0, { + "expected": str(dict(expected_tool_calls_count)), + "actual": str(dict(actual_tool_calls_count)), + justification_key: { + "_result": "Both expected and actual tool calls are empty" + }, + } + elif not expected_tool_calls_count or not actual_tool_calls_count: + return 0.0, { + "expected": str(dict(expected_tool_calls_count)), + "actual": str(dict(actual_tool_calls_count)), + justification_key: { + "_result": "Either expected or actual tool calls are empty" + }, + } + + score = 0.0 + justifications: dict[str, Any] = { + "expected": str(dict(expected_tool_calls_count)), + "actual": str(dict(actual_tool_calls_count)), + justification_key: {}, + } + for tool_name, ( + expected_comparator, + expected_count, + ) in expected_tool_calls_count.items(): + actual_count = actual_tool_calls_count.get(tool_name, 0.0) + comparator = f"__{COMPARATOR_MAPPINGS[expected_comparator]}__" + to_add = float(getattr(actual_count, comparator)(expected_count)) + + justifications[justification_key][tool_name] = ( + f"Actual: {actual_count}, Expected: {expected_count}, Score: {to_add}" + ) + if strict and to_add == 0.0: + # When strict is True, if the actual count does not match the expected count, return 0 + # The justification should only include the breaching tool name + return 0.0, { + "expected": str(dict(expected_tool_calls_count)), + "actual": str(dict(actual_tool_calls_count)), + justification_key: { + tool_name: justifications[justification_key][tool_name] + }, + } + score += to_add + return score / len(expected_tool_calls_count), justifications + + +def tool_calls_args_score( + actual_tool_calls: list[ToolCall], + expected_tool_calls: list[ToolCall], + strict: bool = False, + subset: bool = False, + justification_key: str = "explained_tool_calls_args", +) -> tuple[float, dict[str, Any]]: + """Check if the expected tool calls are correctly called with matching arguments. + + This function does not check the order of the tool calls! + + Args: + actual_tool_calls: List of actual tool calls with their arguments. + expected_tool_calls: List of expected tool calls with their arguments. + strict: If True, the function will return 0 if not all expected tool calls are matched. + subset: If True, the function will check if the expected args are a subset of the actual args. + justification_key: Key to use for the justification in the returned dict. + + Returns: + tuple[float, dict]: Score based on the number of matches, and the justification dict. + """ + if not expected_tool_calls and not actual_tool_calls: + return 1.0, { + "expected": str(expected_tool_calls), + "actual": str(actual_tool_calls), + justification_key: { + "_result": "Both expected and actual tool calls are empty" + }, + } + elif not expected_tool_calls or not actual_tool_calls: + return 0.0, { + "expected": str(expected_tool_calls), + "actual": str(actual_tool_calls), + justification_key: { + "_result": "Either expected or actual tool calls are empty" + }, + } + + cnt = 0 + visited: set[int] = set() + justifications: dict[str, Any] = { + "expected": str(expected_tool_calls), + "actual": str(actual_tool_calls), + justification_key: {}, + } + tool_counters: dict[str, int] = {} + + for expected_tool_call in expected_tool_calls: + for idx, call in enumerate(actual_tool_calls): + if call.name == expected_tool_call.name and idx not in visited: + # Get or initialize counter for this tool name + tool_counters[call.name] = tool_counters.get(call.name, 0) + tool_key = f"{call.name}_{tool_counters[call.name]}" + tool_counters[call.name] += 1 + + # Check arguments based on mode + if subset: + # Subset mode: safely check if all expected args exist and match + # Capture 'call' as a default argument to bind the loop variable + args_check = ( # noqa: E731 + lambda k, v, call=call: k in call.args and call.args[k] == v + ) + else: + # Exact mode: direct access (may raise KeyError) + # Capture 'call' as a default argument to bind the loop variable + args_check = lambda k, v, call=call: call.args[k] == v # noqa: E731 + + try: + args_match = all( + args_check(k, v) for k, v in expected_tool_call.args.items() + ) + except KeyError: + # Only possible in exact mode when key is missing + args_match = False + + justifications[justification_key][tool_key] = ( + f"Actual: {call.args}, Expected: {expected_tool_call.args}, Score: {float(args_match)}" + ) + if args_match: + cnt += 1 + visited.add(idx) + break + # In case of mismatch, DON'T add to visited in non-strict mode + # so this actual tool call can be matched against other expected calls + + return ( + cnt / len(expected_tool_calls) + if not strict + else float(cnt == len(expected_tool_calls)) + ), justifications + + +def tool_calls_output_score( + actual_tool_calls_outputs: list[ToolOutput], + expected_tool_calls_outputs: list[ToolOutput], + strict: bool = False, + justification_key: str = "explained_tool_calls_outputs", +) -> tuple[float, dict[str, Any]]: + """Check if the expected tool calls are correctly called, where expected args must be a subset of actual args. + + Args: + actual_tool_calls_outputs: List of actual tool calls outputs. + expected_tool_calls_outputs: List of expected tool calls outputs. + strict: If True, the function will return 0 if not all expected tool calls are matched. + + Returns: + tuple[float, str]: Score based on the number of matches, and the justification. + """ + if not expected_tool_calls_outputs and not actual_tool_calls_outputs: + return 1.0, { + "expected": str(expected_tool_calls_outputs), + "actual": str(actual_tool_calls_outputs), + justification_key: { + "_result": "Both expected and actual tool calls outputs are empty" + }, + } + elif not expected_tool_calls_outputs or not actual_tool_calls_outputs: + return 0.0, { + "expected": str(expected_tool_calls_outputs), + "actual": str(actual_tool_calls_outputs), + justification_key: { + "_result": "Either expected or actual tool calls outputs are empty" + }, + } + + cnt = 0.0 + justifications: dict[str, Any] = { + "expected": str(expected_tool_calls_outputs), + "actual": str(actual_tool_calls_outputs), + justification_key: {}, + } + visited: set[int] = set() + tool_counters: dict[str, int] = {} + + for expected_tool_call_output in expected_tool_calls_outputs: + matched = False + + # Look through ALL actual tool calls to find a match + for idx, actual_tool_call_output in enumerate(actual_tool_calls_outputs): + if idx in visited: + continue + if actual_tool_call_output.name == expected_tool_call_output.name: + # Get or initialize counter for this tool name + tool_counters[actual_tool_call_output.name] = tool_counters.get( + actual_tool_call_output.name, 0 + ) + tool_key = f"{actual_tool_call_output.name}_{tool_counters[actual_tool_call_output.name]}" + tool_counters[actual_tool_call_output.name] += 1 + + justifications[justification_key][tool_key] = ( + f"Actual: {actual_tool_call_output.output}, Expected: {expected_tool_call_output.output}, Score: {float(actual_tool_call_output.output == expected_tool_call_output.output)}" + ) + + if actual_tool_call_output.output == expected_tool_call_output.output: + # Perfect match found + cnt += 1.0 + visited.add(idx) + matched = True + break + elif strict: + # In strict mode, any mismatch returns 0 immediately + return 0.0, { + "expected": str(expected_tool_calls_outputs), + "actual": str(actual_tool_calls_outputs), + justification_key: { + tool_key: justifications[justification_key][tool_key] + }, + } + # In non-strict mode with mismatch, continue looking for perfect match + # DON'T add to visited, DON'T break + + # If no match found and we're in strict mode, return 0 + if not matched and strict: + return 0.0, { + "expected": str(expected_tool_calls_outputs), + "actual": str(actual_tool_calls_outputs), + justification_key: { + "_result": f"No matching actual tool call found for expected {expected_tool_call_output.name}" + }, + } + + return ( + cnt / len(expected_tool_calls_outputs) + if not strict + else float(cnt == len(expected_tool_calls_outputs)) + ), justifications + + +def trace_to_str(agent_trace: Sequence[ReadableSpan]) -> str: + """Convert OTEL spans to a platform-style agent run history string. + + Creates a similar structure to LangChain message processing but using OTEL spans. + Only processes tool spans (spans with 'tool.name' attribute). + + Args: + agent_trace: List of ReadableSpan objects from the agent execution + + Returns: + String representation of the agent run history in platform format + """ + platform_history = [] + seen_tool_calls = set() + + for span in agent_trace: + if span.attributes and (tool_name := span.attributes.get("tool.name")): + # Get span timing information + start_time = span.start_time + end_time = span.end_time + + # Convert nanoseconds to datetime if needed + if isinstance(start_time, int): + start_timestamp = datetime.fromtimestamp(start_time / 1e9) + else: + start_timestamp = start_time # type:ignore + + if isinstance(end_time, int): + end_timestamp = datetime.fromtimestamp(end_time / 1e9) + else: + end_timestamp = end_time # type:ignore + + timestamp_str = ( + start_timestamp.strftime("%Y-%m-%d %H:%M:%S") if start_timestamp else "" + ) + + # Get tool call information + tool_args: Any = span.attributes.get("input.value", {}) + tool_result = str(span.attributes.get("output.value", {})).strip() + + span_id = ( + span.context.span_id + if span.context + else str(hash(f"{tool_name}_{timestamp_str}")) + ) + + # De-duplicate tool calls based on span ID + if span_id in seen_tool_calls: + continue + seen_tool_calls.add(span_id) + + # Add tool selection (equivalent to AIMessage with tool_calls) + platform_history.append(f"[{timestamp_str}] LLM Response:") + platform_history.append(" Agent Selected 1 Tool(s):") + platform_history.append("") + platform_history.append(f" Tool: {tool_name}") + platform_history.append(f" Arguments: {str(tool_args)}") + platform_history.append("") + + # Add tool response (equivalent to ToolMessage) + end_timestamp_str = ( + end_timestamp.strftime("%Y-%m-%d %H:%M:%S") + if end_timestamp + else timestamp_str + ) + platform_history.append( + f"[{end_timestamp_str}] Tool Call Response - {tool_name}:" + ) + platform_history.append(f"{tool_result}") + platform_history.append("") + + return "\n".join(platform_history) diff --git a/packages/uipath-eval/src/uipath_eval/_helpers/helpers.py b/packages/uipath-eval/src/uipath_eval/_helpers/helpers.py new file mode 100644 index 000000000..e69976955 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/_helpers/helpers.py @@ -0,0 +1,57 @@ +"""Helper functions for evaluation process.""" + +import functools +import time +from collections.abc import Callable +from typing import Any + +from ..models import ErrorEvaluationResult, EvaluationResult + + +def is_empty_value(value: Any) -> bool: + """Check if a value is empty or contains only empty values. + + Handles multiple cases: + - None or empty string + - String with only whitespace + - Dict where all values are empty strings or whitespace + - Empty list or dict + """ + if value is None: + return True + + if isinstance(value, str): + return not value.strip() + + if isinstance(value, dict): + if not value: # Empty dict + return True + # Check if all values are empty strings + return all(isinstance(v, str) and not v.strip() for v in value.values()) + + if isinstance(value, list): + return len(value) == 0 + + return False + + +def track_evaluation_metrics(func: Callable[..., Any]) -> Callable[..., Any]: + """Decorator to track evaluation metrics and handle errors gracefully.""" + + @functools.wraps(func) + async def wrapper(*args: Any, **kwargs: Any) -> EvaluationResult: + start_time = time.time() + try: + result = await func(*args, **kwargs) + except Exception as e: + result = ErrorEvaluationResult( + details="Exception thrown by evaluator: {}".format(e), + evaluation_time=time.time() - start_time, + ) + end_time = time.time() + execution_time = end_time - start_time + + result.evaluation_time = execution_time + return result + + return wrapper diff --git a/packages/uipath-eval/src/uipath_eval/_helpers/output_path.py b/packages/uipath-eval/src/uipath_eval/_helpers/output_path.py new file mode 100644 index 000000000..5c50a34df --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/_helpers/output_path.py @@ -0,0 +1,89 @@ +"""Utility for resolving dot-notation paths from agent output dictionaries. + +Supports: +- "*" → return entire output (default behavior) +- "result" → flat key lookup (backward compatible) +- "result.calculation.value" → nested dot-notation path +- "items[0].name" → array index access with dot-notation +- "items[2].details.score" → mixed nested object and array access + +Examples: + >>> resolve_output_path({"result": {"value": 42}}, "result.value") + 42 + >>> resolve_output_path({"items": [{"name": "a"}, {"name": "b"}]}, "items[1].name") + 'b' + >>> resolve_output_path({"result": 5}, "*") + {"result": 5} +""" + +import re +from typing import Any + + +def resolve_output_path(output: Any, path: str) -> Any: + """Resolve a dot-notation path with optional array indexing from output. + + Args: + output: The output dictionary (or any value) to resolve the path from. + path: The path string. "*" returns the full output. + Dot notation for nested objects: "a.b.c" + Bracket notation for array indices: "items[0].name" + + Returns: + The resolved value at the given path. + + Raises: + KeyError: If a dict key in the path is not found. + IndexError: If an array index is out of range. + TypeError: If the path tries to index into a non-dict/non-list value. + """ + if path == "*": + return output + + tokens = _tokenize_path(path) + current = output + + for token in tokens: + if isinstance(token, int): + if not isinstance(current, (list, tuple)): + raise TypeError( + f"Cannot index into {type(current).__name__} with integer index [{token}]" + ) + current = current[token] + else: + if not isinstance(current, dict): + raise TypeError( + f"Cannot access key '{token}' on {type(current).__name__}" + ) + current = current[token] + + return current + + +# Pattern to match either: +# - a bare key segment (no dots or brackets) +# - a bracket index like [0], [12] +_TOKEN_PATTERN = re.compile(r"([^.\[\]]+)|\[(\d+)\]") + + +def _tokenize_path(path: str) -> list[str | int]: + """Parse a path string into a list of string keys and integer indices. + + Examples: + "result" → ["result"] + "result.value" → ["result", "value"] + "items[0].name" → ["items", 0, "name"] + "data[2].nested.list[0]" → ["data", 2, "nested", "list", 0] + """ + tokens: list[str | int] = [] + for match in _TOKEN_PATTERN.finditer(path): + key_part, index_part = match.groups() + if index_part is not None: + tokens.append(int(index_part)) + else: + tokens.append(key_part) + + if not tokens: + raise ValueError(f"Invalid path: '{path}'") + + return tokens diff --git a/packages/uipath-eval/src/uipath_eval/constants.py b/packages/uipath-eval/src/uipath_eval/constants.py new file mode 100644 index 000000000..39f45b2e3 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/constants.py @@ -0,0 +1,5 @@ +"""Constants for uipath-eval package.""" + +LEGACY_EVAL_FOLDER = "evals" +EVALS_FOLDER = "evaluations" +CUSTOM_EVALUATOR_PREFIX = "file://" diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/__init__.py b/packages/uipath-eval/src/uipath_eval/evaluators/__init__.py new file mode 100644 index 000000000..a7f394647 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/__init__.py @@ -0,0 +1,61 @@ +"""UiPath evaluator implementations for agent performance evaluation. + +Platform-dependent evaluators (LLMAsAJudge, LegacyContextPrecision, +LegacyFaithfulness, LegacyTrajectory) require uipath-platform and +remain in the uipath package. +""" + +from typing import Any + +from .base_evaluator import ( + BaseEvaluationCriteria, + BaseEvaluator, + BaseEvaluatorConfig, + BaseEvaluatorJustification, + GenericBaseEvaluator, +) +from .base_legacy_evaluator import BaseLegacyEvaluator +from .binary_classification_evaluator import BinaryClassificationEvaluator +from .contains_evaluator import ContainsEvaluator +from .exact_match_evaluator import ExactMatchEvaluator +from .json_similarity_evaluator import JsonSimilarityEvaluator +from .legacy_exact_match_evaluator import LegacyExactMatchEvaluator +from .legacy_json_similarity_evaluator import LegacyJsonSimilarityEvaluator +from .multiclass_classification_evaluator import MulticlassClassificationEvaluator +from .tool_call_args_evaluator import ToolCallArgsEvaluator +from .tool_call_count_evaluator import ToolCallCountEvaluator +from .tool_call_order_evaluator import ToolCallOrderEvaluator +from .tool_call_output_evaluator import ToolCallOutputEvaluator + +EVALUATORS: list[type[BaseEvaluator[Any, Any, Any]]] = [ + ExactMatchEvaluator, + ContainsEvaluator, + BinaryClassificationEvaluator, + MulticlassClassificationEvaluator, + JsonSimilarityEvaluator, + ToolCallOrderEvaluator, + ToolCallArgsEvaluator, + ToolCallCountEvaluator, + ToolCallOutputEvaluator, +] + +__all__ = [ + "EVALUATORS", + "BaseEvaluationCriteria", + "BaseEvaluator", + "BaseEvaluatorConfig", + "BaseEvaluatorJustification", + "GenericBaseEvaluator", + "BaseLegacyEvaluator", + "BinaryClassificationEvaluator", + "ContainsEvaluator", + "ExactMatchEvaluator", + "JsonSimilarityEvaluator", + "LegacyExactMatchEvaluator", + "LegacyJsonSimilarityEvaluator", + "MulticlassClassificationEvaluator", + "ToolCallArgsEvaluator", + "ToolCallCountEvaluator", + "ToolCallOrderEvaluator", + "ToolCallOutputEvaluator", +] diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/base_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/base_evaluator.py new file mode 100644 index 000000000..80e13e1c0 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/base_evaluator.py @@ -0,0 +1,642 @@ +"""Base evaluator abstract class for agent evaluation.""" + +import json +from abc import ABC, abstractmethod +from typing import Any, Generic, TypeVar, Union, cast, get_args + +from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic.alias_generators import to_camel + +from .._helpers.helpers import track_evaluation_metrics +from ..models import AgentExecution, EvaluationResult +from ..models.models import ( + EvaluationResultDto, + UiPathEvaluationError, + UiPathEvaluationErrorCategory, +) + + +class BaseEvaluationCriteria(BaseModel): + """Base class for all evaluation criteria.""" + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + pass + + +# Type variable for evaluation criteria, used by both Config and Evaluator +T = TypeVar("T", bound=BaseEvaluationCriteria) + + +class BaseEvaluatorConfig(BaseModel, Generic[T]): + """Base class for all evaluator configurations. + + Generic over T (evaluation criteria type) to ensure type safety between + the config's default_evaluation_criteria and the evaluator's expected criteria type. + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + name: str = Field(description="The name of the evaluator") + description: str = Field(default="", description="The description of the evaluator") + default_evaluation_criteria: T | None = None + + +class BaseEvaluatorJustification(BaseModel): + """Base class for all evaluator justifications.""" + + expected: str + actual: str + + +# Additional type variables for Config and Justification +# Note: C must be BaseEvaluatorConfig[T] to ensure type consistency +C = TypeVar("C", bound=BaseEvaluatorConfig[Any]) +J = TypeVar("J", bound=Union[str, BaseEvaluatorJustification]) + + +class GenericBaseEvaluator(BaseModel, Generic[T, C, J], ABC): + """Abstract base class for all evaluators. + + Generic Parameters: + T: The evaluation criteria type (bound to BaseEvaluationCriteria) + C: The evaluator config type (bound to BaseEvaluatorConfig[T]) + J: The justification type (str, None, or BaseEvaluatorJustification subclass) + + Design Rationale: + T is explicitly specified even though C = BaseEvaluatorConfig[T] already encodes it. + This redundancy is intentional and provides: + + 1. **Type Checker Support**: Static type checkers can infer the exact criteria type + for the evaluate() method signature without runtime introspection + + 2. **Clear API**: The signature BaseEvaluator[MyCriteria, MyConfig[MyCriteria], str] + makes it immediately obvious what criteria type is expected + + 3. **IDE Support**: Autocomplete and type hints work perfectly for method parameters + + Runtime validation ensures T and C's generic parameter are consistent. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + id: str + name: str = Field(default="", description="The name of the evaluator") + description: str = Field(default="", description="The description of the evaluator") + + config_type: type[C] = Field(description="The config type class", exclude=True) + evaluation_criteria_type: type[T] = Field( + description="The type used for evaluation criteria validation and creation", + exclude=True, + ) + justification_type: type[J] = Field( + description="The type used for justification validation and creation", + exclude=True, + ) + + def __init_subclass__(cls, **kwargs: Any): + """Hook for subclass creation - automatically applies evaluation metrics tracking.""" + super().__init_subclass__(**kwargs) + + if hasattr(cls, "evaluate") and not getattr( + cls.evaluate, "_has_metrics_decorator", False + ): + new_evaluation_method = track_evaluation_metrics(cls.evaluate) + new_evaluation_method._has_metrics_decorator = True # type: ignore[attr-defined] # probably a better way to do this + cls.evaluate = new_evaluation_method # type: ignore[method-assign] # probably a better way to do this + + @model_validator(mode="before") + @classmethod + def validate_model(cls, values: Any) -> Any: + """Pre-initialization model validator for Pydantic models. + + This validator extracts the Generic type parameters and validates their consistency. + + Args: + values: The raw input values before validation + + Returns: + The validated/transformed values with types set + + Raises: + ValueError: If types cannot be determined or are inconsistent + """ + if isinstance(values, dict): + if "description" in values and "evaluatorConfig" in values: + values["evaluatorConfig"]["description"] = values.pop("description") + if "name" in values and "evaluatorConfig" in values: + values["evaluatorConfig"]["name"] = values.pop("name") + # Always extract and set evaluation_criteria_type + criteria_type = cls._extract_evaluation_criteria_type() + values["evaluation_criteria_type"] = criteria_type + + # Always extract and set config_type + config_type = cls._extract_config_type() + values["config_type"] = config_type + + # Always extract and set justification_type + justification_type = cls._extract_justification_type() + values["justification_type"] = justification_type + + # Validate consistency: config's generic parameter should match criteria_type + cls._validate_type_consistency(config_type, criteria_type) + + # Validate and create the config object if config dict is provided + try: + raw_config = values.get("config") or values.get("evaluatorConfig") or {} + validated_config = config_type.model_validate(raw_config) + values["evaluator_config"] = validated_config + except Exception as e: + raise UiPathEvaluationError( + code="FAILED_TO_VALIDATE_EVALUATOR_CONFIG", + title=f"Failed to validate evaluator config for {cls.__name__}", + detail=f"Error: {e}", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) from e + + return values + + @classmethod + def _validate_type_consistency( + cls, + config_type: type[BaseEvaluatorConfig[Any]], + criteria_type: type[BaseEvaluationCriteria], + ) -> None: + """Validate that the config's generic parameter matches the evaluator's criteria type. + + Extracts the criteria type from the config's default_evaluation_criteria field + annotation and validates it matches the evaluator's expected criteria type. + + Args: + config_type: The config type to validate + criteria_type: The expected evaluation criteria type + + Raises: + ValueError: If the types are inconsistent + """ + # Skip validation for base classes + if config_type.__name__ in ( + "BaseEvaluatorConfig", + "OutputEvaluatorConfig", + "BaseLLMJudgeEvaluatorConfig", + ): + return + + # Extract from Pydantic's model_fields which preserves generic types + if ( + hasattr(config_type, "model_fields") + and "default_evaluation_criteria" in config_type.model_fields + ): + field_info = config_type.model_fields["default_evaluation_criteria"] + if hasattr(field_info, "annotation"): + annotation = field_info.annotation + # The annotation will be SomeCriteria | None + args = get_args(annotation) + if args: + # Get the criteria type (the non-None arg) + for arg in args: + if ( + arg is not type(None) + and isinstance(arg, type) + and issubclass(arg, BaseEvaluationCriteria) + ): + # Found the config's criteria type, check if it matches + if arg != criteria_type: + raise UiPathEvaluationError( + code="TYPE_INCONSISTENCY_IN_EVALUATOR", + title=f"Type inconsistency in {cls.__name__}: " + f"Config {config_type.__name__} expects criteria type {arg.__name__}", + detail=f"Evaluator expects {criteria_type.__name__}. " + f"Ensure BaseEvaluator[T, C[T], J] has matching T and C[T] parameters.", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + return # Validation passed + + @classmethod + def _extract_evaluation_criteria_type(cls) -> type[BaseEvaluationCriteria]: + """Extract the evaluation criteria type from Pydantic model fields. + + Returns: + The evaluation criteria type + + Raises: + ValueError: If no valid evaluation criteria type can be determined from the class definition + """ + # Special case: if this is the BaseEvaluator class itself, return BaseEvaluationCriteria + if cls.__name__ in ("BaseEvaluator", "BaseEvaluator[Any, Any, Any]"): + return BaseEvaluationCriteria + + # Check if Pydantic has already resolved the evaluation_criteria_type field annotation + if not ( + hasattr(cls, "model_fields") + and "evaluation_criteria_type" in cls.model_fields + ): + raise UiPathEvaluationError( + code="COULD_NOT_FIND_EVALUATION_CRITERIA_TYPE_FIELD", + title=f"Could not find evaluation_criteria_type field in {cls.__name__}", + detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + field_info = cls.model_fields["evaluation_criteria_type"] + if not hasattr(field_info, "annotation"): + raise UiPathEvaluationError( + code="NO_ANNOTATION_FOUND_FOR_EVALUATION_CRITERIA_TYPE_FIELD", + title=f"No annotation found for evaluation_criteria_type field in {cls.__name__}", + detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + # Extract the inner type from type[SomeType] + annotation = field_info.annotation + args = get_args(annotation) + if not args: + raise UiPathEvaluationError( + code="INVALID_ANNOTATION_FOR_EVALUATION_CRITERIA_TYPE", + title=f"Invalid annotation for evaluation_criteria_type in {cls.__name__}: {annotation}", + detail="Expected type[SomeEvaluationCriteria]", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + criteria_type = args[0] + if not ( + isinstance(criteria_type, type) + and issubclass(criteria_type, BaseEvaluationCriteria) + ): + raise UiPathEvaluationError( + code="INVALID_EVALUATION_CRITERIA_TYPE", + title=f"Invalid evaluation criteria type {criteria_type} in {cls.__name__}", + detail=f"{criteria_type} must be a subclass of BaseEvaluationCriteria", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + return criteria_type + + @classmethod + def _extract_config_type(cls) -> type[BaseEvaluatorConfig[Any]]: + """Extract the config type from Pydantic model fields. + + Returns: + The config type for this evaluator + + Raises: + ValueError: If no valid config type can be determined from the class definition + """ + # Special case: if this is the BaseEvaluator class itself, return BaseEvaluatorConfig + if cls.__name__ in ("BaseEvaluator", "BaseEvaluator[Any, Any, Any]"): + return BaseEvaluatorConfig + # Check if Pydantic has already resolved the config_type field annotation + if not (hasattr(cls, "model_fields") and "config_type" in cls.model_fields): + raise UiPathEvaluationError( + code="COULD_NOT_FIND_CONFIG_TYPE_FIELD", + title=f"Could not find config_type field in {cls.__name__}", + detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + field_info = cls.model_fields["config_type"] + if not hasattr(field_info, "annotation"): + raise UiPathEvaluationError( + code="NO_ANNOTATION_FOUND_FOR_CONFIG_TYPE_FIELD", + title=f"No annotation found for config_type field in {cls.__name__}", + detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + # Extract the inner type from type[SomeType] + annotation = field_info.annotation + args = get_args(annotation) + if not args: + raise UiPathEvaluationError( + code="INVALID_ANNOTATION_FOR_CONFIG_TYPE", + title=f"Invalid annotation for config_type in {cls.__name__}: {annotation}", + detail="Expected type[SomeEvaluatorConfig]", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + config_type = args[0] + if not ( + isinstance(config_type, type) + and issubclass(config_type, BaseEvaluatorConfig) + ): + raise UiPathEvaluationError( + code="INVALID_CONFIG_TYPE", + title=f"Invalid config type {config_type} in {cls.__name__}", + detail=f"{config_type} must be a subclass of BaseEvaluatorConfig", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + return config_type + + @classmethod + def _extract_justification_type(cls) -> type[J]: + """Extract the justification type from Pydantic model fields. + + Returns: + The justification type (str or BaseEvaluatorJustification subclass) + + Raises: + UiPathEvaluationError: If no valid justification type can be determined + """ + try: + # Check if Pydantic has resolved the justification_type field annotation + if not ( + hasattr(cls, "model_fields") + and "justification_type" in cls.model_fields + ): + raise UiPathEvaluationError( + code="COULD_NOT_FIND_JUSTIFICATION_TYPE_FIELD", + title=f"Could not find justification_type field in {cls.__name__}", + detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + field_info = cls.model_fields["justification_type"] + if not hasattr(field_info, "annotation"): + raise UiPathEvaluationError( + code="NO_ANNOTATION_FOUND_FOR_JUSTIFICATION_TYPE_FIELD", + title=f"No annotation found for justification_type field in {cls.__name__}", + detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + # Extract the inner type from type[SomeType] + annotation = field_info.annotation + args = get_args(annotation) + if not args: + raise UiPathEvaluationError( + code="INVALID_ANNOTATION_FOR_JUSTIFICATION_TYPE", + title=f"Invalid annotation for justification_type in {cls.__name__}: {annotation}", + detail="Expected type[str] or type[SomeEvaluatorJustification]", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + justification_type = args[0] + + # Validate the justification type - must be str or BaseEvaluatorJustification subclass + if justification_type is str: + return cast(type[J], justification_type) + elif isinstance(justification_type, type) and issubclass( + justification_type, BaseEvaluatorJustification + ): + return cast(type[J], justification_type) + else: + raise UiPathEvaluationError( + code="INVALID_JUSTIFICATION_TYPE", + title=f"Invalid justification type {justification_type} in {cls.__name__}", + detail="Must be str or subclass of BaseEvaluatorJustification.", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + except UiPathEvaluationError: + raise + except Exception as e: + raise UiPathEvaluationError( + code="CANNOT_EXTRACT_JUSTIFICATION_TYPE", + title=f"Cannot extract justification type from {cls.__name__}", + detail=f"Error: {e}", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) from e + + def validate_evaluation_criteria(self, criteria: Any) -> T: + """Validate and convert input to the correct evaluation criteria type. + + Uses Pydantic's model_validate for proper validation, type coercion, + and error handling. + + Args: + criteria: The criteria to validate (dict, BaseEvaluationCriteria, or other) + + Returns: + An instance of the evaluation criteria type (T) + + Raises: + ValueError: If the criteria cannot be converted to the expected type + """ + try: + if isinstance(criteria, self.evaluation_criteria_type): + return criteria + elif isinstance(criteria, dict): + return self.evaluation_criteria_type.model_validate(criteria) + elif hasattr(criteria, "__dict__"): + # Try to convert from another object type + return self.evaluation_criteria_type.model_validate(criteria.__dict__) + else: + # Try to let Pydantic handle the conversion + return self.evaluation_criteria_type.model_validate(criteria) + except Exception as e: + raise UiPathEvaluationError( + code="CANNOT_VALIDATE_EVALUATION_CRITERIA", + title=f"Cannot validate {type(criteria)} to {self.evaluation_criteria_type}", + detail=f"Error: {e}", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) from e + + def validate_justification(self, justification: Any) -> J: + """Validate and convert input to the correct justification type. + + Args: + justification: The justification to validate (str, dict, BaseEvaluatorJustification, or other) + + Returns: + The validated justification of the correct type + """ + try: + # Handle str type - when J is bound to str + if self.justification_type is str: + if justification is None: + return cast(J, "") + return cast(J, str(justification)) + + # Handle BaseEvaluatorJustification subclasses - when J is bound to a specific subclass + if isinstance(self.justification_type, type) and issubclass( + self.justification_type, BaseEvaluatorJustification + ): + if justification is None: + raise ValueError( + f"None is not allowed for justification type {self.justification_type}" + ) + + if isinstance(justification, self.justification_type): + return justification + elif isinstance(justification, dict): + return self.justification_type.model_validate(justification) + elif hasattr(justification, "__dict__"): + return self.justification_type.model_validate( + justification.__dict__ + ) + else: + return self.justification_type.model_validate(justification) + except Exception as e: + raise UiPathEvaluationError( + code="CANNOT_CONVERT_JUSTIFICATION", + title=f"Cannot convert {type(justification)} to {self.justification_type}", + detail=f"Error: {e}", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) from e + + # Fallback: this should never happen + raise UiPathEvaluationError( + code="UNSUPPORTED_JUSTIFICATION_TYPE", + title=f"Unsupported justification type {self.justification_type} for input {type(justification)}", + detail=f"Unsupported justification type {self.justification_type} for input {type(justification)}", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + @classmethod + def get_evaluation_criteria_schema(cls) -> dict[str, Any]: + """Get the JSON schema for the evaluation criteria type. + + Returns: + The JSON schema for the evaluation criteria type + """ + criteria_type = cls._extract_evaluation_criteria_type() + return criteria_type.model_json_schema(by_alias=False) + + @classmethod + def get_config_schema(cls) -> dict[str, Any]: + """Get the JSON schema for the config type. + + Returns: + The JSON schema for the config type + """ + config_type = cls._extract_config_type() + return config_type.model_json_schema(by_alias=False) + + @classmethod + def get_justification_schema(cls) -> dict[str, Any]: + """Get the JSON schema for the justification type. + + Returns: + The JSON schema for the justification type + """ + justification_type = cls._extract_justification_type() + if justification_type is str: + return {"type": "string"} + elif isinstance(justification_type, type) and issubclass( + justification_type, BaseEvaluatorJustification + ): + return justification_type.model_json_schema(by_alias=False) + else: + raise UiPathEvaluationError( + code="INVALID_JUSTIFICATION_TYPE", + title=f"Invalid justification type {justification_type} in {cls.__name__}", + detail="Must be str or subclass of BaseEvaluatorJustification", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + def _canonical_json(self, obj: Any) -> str: + """Convert an object to canonical JSON string for consistent comparison. + + Args: + obj: The object to convert to canonical JSON + + Returns: + str: Canonical JSON string with normalized numbers and sorted keys + """ + return json.dumps( + obj, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ) + + @classmethod + @abstractmethod + def get_evaluator_id(cls) -> str: + """Get the evaluator id.""" + pass + + @classmethod + def generate_json_type(cls) -> dict[str, Any]: + """Generate the JSON schema for the evaluator.""" + return { + "evaluatorTypeId": cls.get_evaluator_id(), + "evaluatorConfigSchema": cls.get_config_schema(), + "evaluationCriteriaSchema": cls.get_evaluation_criteria_schema(), + "justificationSchema": cls.get_justification_schema(), + } + + def reduce_scores(self, results: list[EvaluationResultDto]) -> float: + """Reduce per-datapoint results into a single aggregated score. + + Default implementation computes a simple average of scores. Subclasses + can override this to implement custom aggregation logic (e.g., precision, + recall) using the rich per-datapoint data in EvaluationResultDto. + + Args: + results: List of per-datapoint results, each containing the score + and evaluation details/justification. + + Returns: + The aggregated score + """ + if not results: + return 0.0 + return sum(r.score for r in results) / len(results) + + @abstractmethod + async def validate_and_evaluate_criteria( + self, agent_execution: AgentExecution, evaluation_criteria: Any + ) -> EvaluationResult: + """Evaluate the given data and return a result from a raw evaluation criteria.""" + pass + + @abstractmethod + async def evaluate( + self, agent_execution: AgentExecution, evaluation_criteria: T + ) -> EvaluationResult: + """Evaluate the given data and return a result. + + Args: + agent_execution: The execution details containing: + - agent_input: The input received by the agent + - agent_output: The actual output from the agent + - agent_trace: The execution trace from the agent + - simulation_instructions: The simulation instructions for the agent + evaluation_criteria: The criteria to evaluate + + Returns: + EvaluationResult containing the score and details + """ + pass + + +class BaseEvaluator(GenericBaseEvaluator[T, C, J]): + """Abstract base class for all coded evaluators. Not naming this BaseCodedEvaluator for backwards compatibility.""" + + version: str = Field(default="1.0", description="Version of the evaluator") + evaluator_type_id: str = Field( + default="", alias="evaluatorTypeId", description="Type of the evaluator" + ) + evaluator_config: C = Field( + alias="evaluatorConfig", description="The validated config object instance" + ) + + name: str = Field(default="", description="The name of the evaluator", exclude=True) + description: str = Field( + default="", description="The description of the evaluator", exclude=True + ) + + def model_post_init(self, __context: Any) -> None: + """Post initialization of the evaluator.""" + if not self.evaluator_type_id: + self.evaluator_type_id = type(self).get_evaluator_id() + if not self.name: + self.name = self.evaluator_config.name + if not self.description: + self.description = self.evaluator_config.description + + async def validate_and_evaluate_criteria( + self, agent_execution: AgentExecution, evaluation_criteria: Any + ) -> EvaluationResult: + """Evaluate the given data and return a result from a raw evaluation criteria.""" + if evaluation_criteria is None: + evaluation_criteria = self.evaluator_config.default_evaluation_criteria + if evaluation_criteria is None: + raise UiPathEvaluationError( + code="NO_EVALUATION_CRITERIA_PROVIDED", + title="No evaluation criteria provided and no default evaluation criteria configured", + detail="No evaluation criteria provided and no default evaluation criteria configured", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + criteria = self.validate_evaluation_criteria(evaluation_criteria) + return await self.evaluate(agent_execution, criteria) diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/base_legacy_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/base_legacy_evaluator.py new file mode 100644 index 000000000..2a3e6146c --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/base_legacy_evaluator.py @@ -0,0 +1,106 @@ +"""Base evaluator abstract class for agent evaluation.""" + +from abc import ABC, abstractmethod +from typing import Any, Generic, TypeVar + +from pydantic import ConfigDict, Field + +from .._helpers.helpers import track_evaluation_metrics +from ..models import EvaluationResult +from ..models.models import ( + AgentExecution, + LegacyEvaluatorCategory, + LegacyEvaluatorType, +) +from .base_evaluator import ( + BaseEvaluationCriteria, + BaseEvaluatorConfig, + GenericBaseEvaluator, +) + +__all__ = ["track_evaluation_metrics"] + + +# Legacy evaluator config (non-generic version for simplicity) +class LegacyEvaluatorConfig(BaseEvaluatorConfig[BaseEvaluationCriteria]): + """Configuration for legacy evaluators.""" + + name: str = "LegacyEvaluator" + default_evaluation_criteria: None = None # Legacy evaluators don't use this + + +class LegacyEvaluationCriteria(BaseEvaluationCriteria): + """Legacy evaluation criteria.""" + + expected_output: Any = Field(alias="expectedOutput") + expected_agent_behavior: str = Field(alias="expectedAgentBehavior") + + +T = TypeVar("T", bound=LegacyEvaluatorConfig) + + +class BaseLegacyEvaluator( + GenericBaseEvaluator[LegacyEvaluationCriteria, T, str], Generic[T], ABC +): + """Abstract base class for all legacy evaluators. + + Inherits from BaseEvaluator to share common evaluator infrastructure while maintaining + legacy-specific fields and behavior. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + # Required Fields + category: LegacyEvaluatorCategory = Field(...) + type: LegacyEvaluatorType = Field(...) + + # Optional Fields + file_name: str = Field(default="", alias="fileName") + target_output_key: str = Field(default="*", alias="targetOutputKey") + created_at: str = Field(..., alias="createdAt") + updated_at: str = Field(..., alias="updatedAt") + + # Note: __init_subclass__ is inherited from BaseEvaluator and handles metrics tracking + + def model_post_init(self, __context: Any): + """Post-initialization hook for Pydantic models.""" + # Ensure config is set up for legacy evaluators + super().model_post_init(__context) + + @classmethod + def get_evaluator_id(cls) -> str: + """Get the evaluator id. + + For legacy evaluators, this returns a placeholder. Actual evaluator instances + have an 'id' field that identifies them. + """ + return "legacy-evaluator" + + async def validate_and_evaluate_criteria( + self, + agent_execution: AgentExecution, + evaluation_criteria: LegacyEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate the given data and return a result from a raw evaluation criteria.""" + criteria = self.validate_evaluation_criteria(evaluation_criteria) + return await self.evaluate(agent_execution, criteria) + + @abstractmethod + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: LegacyEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate the given data and return a result. + + Args: + agent_execution: The execution details containing: + - agent_input: The input received by the agent + - actual_output: The actual output from the agent + - spans: The execution spans to use for the evaluation + evaluation_criteria: The criteria to evaluate (legacy evaluators accept any type) + + Returns: + EvaluationResult containing the score and details + """ + pass diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/binary_classification_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/binary_classification_evaluator.py new file mode 100644 index 000000000..d56509228 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/binary_classification_evaluator.py @@ -0,0 +1,132 @@ +"""Binary classification evaluator for agent outputs. + +Evaluates binary classification by comparing predicted vs expected class. +Per-datapoint score is 1.0 (correct) or 0.0 (incorrect). The reduce_scores +method reads predicted/expected from justification details to build +TP/FP/FN/TN counts and compute precision, recall, or F-score. +""" + +from typing import Literal + +from ..models import ( + AgentExecution, + EvaluationResult, + EvaluatorType, + NumericEvaluationResult, +) +from ..models.models import ( + EvaluationResultDto, + UiPathEvaluationError, + UiPathEvaluationErrorCategory, +) +from .base_evaluator import BaseEvaluationCriteria, BaseEvaluatorJustification +from .output_evaluator import ( + BaseOutputEvaluator, + OutputEvaluatorConfig, +) + + +class BinaryClassificationEvaluationCriteria(BaseEvaluationCriteria): + """Per-datapoint criteria: which class this sample should belong to.""" + + expected_class: str + + +class BinaryClassificationEvaluatorConfig( + OutputEvaluatorConfig[BinaryClassificationEvaluationCriteria] +): + """Configuration for the binary classification evaluator.""" + + name: str = "BinaryClassificationEvaluator" + positive_class: str + metric_type: Literal["precision", "recall", "f-score"] = "precision" + f_value: float = 1.0 + + +class BinaryClassificationEvaluator( + BaseOutputEvaluator[ + BinaryClassificationEvaluationCriteria, + BinaryClassificationEvaluatorConfig, + BaseEvaluatorJustification, + ] +): + """Binary classification evaluator with precision/recall/F-score aggregation. + + Per-datapoint scores are 1.0 (correct) or 0.0 (incorrect). The reduce_scores + method reads predicted/expected from justification details to compute metrics. + """ + + @classmethod + def get_evaluator_id(cls) -> str: + """Get the evaluator id.""" + return EvaluatorType.BINARY_CLASSIFICATION.value + + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: BinaryClassificationEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate binary classification by comparing predicted vs expected class.""" + predicted_class = str(self._get_actual_output(agent_execution)).lower() + expected_class = evaluation_criteria.expected_class.lower() + positive_class = self.evaluator_config.positive_class.lower() + + if not positive_class: + raise UiPathEvaluationError( + code="INVALID_POSITIVE_CLASS", + title="Positive class is empty", + detail="positive_class must be a non-empty string", + category=UiPathEvaluationErrorCategory.USER, + ) + + score = 1.0 if predicted_class == expected_class else 0.0 + + justification = self.validate_justification( + { + "expected": expected_class, + "actual": predicted_class, + } + ) + return NumericEvaluationResult(score=score, details=justification) + + def reduce_scores(self, results: list[EvaluationResultDto]) -> float: + """Compute precision, recall, or F-score from per-datapoint results.""" + if not results: + return 0.0 + + positive_class = self.evaluator_config.positive_class.lower() + tp = fp = fn = 0 + + for r in results: + if isinstance(r.details, BaseEvaluatorJustification): + details = r.details + elif isinstance(r.details, dict): + try: + details = BaseEvaluatorJustification.model_validate(r.details) + except Exception: + continue + else: + continue + pred = details.actual + exp = details.expected + if pred == positive_class and exp == positive_class: + tp += 1 + elif pred == positive_class: + fp += 1 + elif exp == positive_class: + fn += 1 + + metric_type = self.evaluator_config.metric_type + + if metric_type == "precision": + return tp / (tp + fp) if (tp + fp) > 0 else 0.0 + elif metric_type == "recall": + return tp / (tp + fn) if (tp + fn) > 0 else 0.0 + elif metric_type == "f-score": + p = tp / (tp + fp) if (tp + fp) > 0 else 0.0 + rec = tp / (tp + fn) if (tp + fn) > 0 else 0.0 + beta_sq = self.evaluator_config.f_value**2 + denom = beta_sq * p + rec + return (1 + beta_sq) * p * rec / denom if denom > 0 else 0.0 + else: + return 0.0 diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/contains_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/contains_evaluator.py new file mode 100644 index 000000000..c002b47d6 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/contains_evaluator.py @@ -0,0 +1,90 @@ +"""Contains evaluator for agent outputs.""" + +from ..models import ( + AgentExecution, + EvaluationResult, + EvaluatorType, + NumericEvaluationResult, +) +from .base_evaluator import BaseEvaluationCriteria, BaseEvaluatorJustification +from .output_evaluator import ( + BaseOutputEvaluator, + OutputEvaluatorConfig, +) + + +class ContainsEvaluationCriteria(BaseEvaluationCriteria): + """Evaluation criteria for the contains evaluator.""" + + search_text: str + + +class ContainsEvaluatorConfig(OutputEvaluatorConfig[ContainsEvaluationCriteria]): + """Configuration for the contains evaluator.""" + + name: str = "ContainsEvaluator" + case_sensitive: bool = False + negated: bool = False + + +class ContainsEvaluator( + BaseOutputEvaluator[ + ContainsEvaluationCriteria, ContainsEvaluatorConfig, BaseEvaluatorJustification + ] +): + """Evaluator that checks if the actual output contains the expected output. + + This evaluator returns True if the actual output contains the expected output, + and False otherwise. It supports case sensitivity and negation options. + """ + + @classmethod + def get_evaluator_id(cls) -> str: + """Get the evaluator id.""" + return EvaluatorType.CONTAINS.value + + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: ContainsEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate whether actual output contains the expected output. + + Args: + agent_execution: The execution details containing: + - agent_input: The input received by the agent + - agent_output: The actual output from the agent + - agent_trace: The execution spans to use for the evaluation + evaluation_criteria: The criteria to evaluate + + Returns: + EvaluationResult: Boolean result indicating if output contains expected value (True/False) + """ + actual_output = str(self._get_actual_output(agent_execution)) + expected_output = str(self._get_expected_output(evaluation_criteria)) + + if not self.evaluator_config.case_sensitive: + actual_output = actual_output.lower() + expected_output = expected_output.lower() + + is_contains = expected_output in actual_output + + if self.evaluator_config.negated: + is_contains = not is_contains + + validated_justification = self.validate_justification( + { + "expected": expected_output, + "actual": actual_output, + } + ) + return NumericEvaluationResult( + score=float(is_contains), + details=validated_justification, + ) + + def _get_expected_output( + self, evaluation_criteria: ContainsEvaluationCriteria + ) -> str: + """Get the expected output from the evaluation criteria.""" + return evaluation_criteria.search_text diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/exact_match_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/exact_match_evaluator.py new file mode 100644 index 000000000..0f1b3e8e8 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/exact_match_evaluator.py @@ -0,0 +1,84 @@ +"""Exact match evaluator for agent outputs.""" + +from ..models import ( + AgentExecution, + EvaluationResult, + EvaluatorType, + NumericEvaluationResult, +) +from .base_evaluator import BaseEvaluatorJustification +from .output_evaluator import ( + OutputEvaluationCriteria, + OutputEvaluator, + OutputEvaluatorConfig, +) + + +class ExactMatchEvaluatorConfig(OutputEvaluatorConfig[OutputEvaluationCriteria]): + """Configuration for the exact match evaluator.""" + + name: str = "ExactMatchEvaluator" + case_sensitive: bool = False + negated: bool = False + + +class ExactMatchEvaluator( + OutputEvaluator[ + OutputEvaluationCriteria, ExactMatchEvaluatorConfig, BaseEvaluatorJustification + ] +): + """Evaluator that performs exact structural matching between expected and actual outputs. + + This evaluator returns True if the actual output exactly matches the expected output + after canonical JSON normalization, and False otherwise. Numbers are normalized + to floats for consistent comparison. + """ + + @classmethod + def get_evaluator_id(cls) -> str: + """Get the evaluator id.""" + return EvaluatorType.EXACT_MATCH.value + + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: OutputEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate whether actual output exactly matches expected output. + + Args: + agent_execution: The execution details containing: + - agent_input: The input received by the agent + - agent_output: The actual output from the agent + - agent_trace: The execution spans to use for the evaluation + evaluation_criteria: The criteria to evaluate + + Returns: + EvaluationResult: Boolean result indicating exact match (True/False) + """ + actual_output = self._get_actual_output(agent_execution) + expected_output = self._get_expected_output(evaluation_criteria) + + if isinstance(actual_output, str) or isinstance(expected_output, str): + actual_str = str(actual_output) + expected_str = str(expected_output) + if not self.evaluator_config.case_sensitive: + actual_str = actual_str.lower() + expected_str = expected_str.lower() + is_exact_match = actual_str == expected_str + else: + is_exact_match = actual_output == expected_output + + if self.evaluator_config.negated: + is_exact_match = not is_exact_match + + validated_justification = self.validate_justification( + { + "expected": str(expected_output), + "actual": str(actual_output), + } + ) + return NumericEvaluationResult( + score=float(is_exact_match), + details=validated_justification, + ) diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/json_similarity_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/json_similarity_evaluator.py new file mode 100644 index 000000000..552194f2e --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/json_similarity_evaluator.py @@ -0,0 +1,200 @@ +"""JSON similarity evaluator for flexible structural comparison of outputs.""" + +import math +from typing import Any, Tuple + +from ..models import ( + AgentExecution, + EvaluationResult, + EvaluatorType, + NumericEvaluationResult, +) +from .base_evaluator import BaseEvaluatorJustification +from .output_evaluator import ( + OutputEvaluationCriteria, + OutputEvaluator, + OutputEvaluatorConfig, +) + + +class JsonSimilarityJustification(BaseEvaluatorJustification): + """Justification for the JSON similarity evaluator.""" + + matched_leaves: float + total_leaves: float + + +class JsonSimilarityEvaluatorConfig(OutputEvaluatorConfig[OutputEvaluationCriteria]): + """Configuration for the json similarity evaluator.""" + + name: str = "JsonSimilarityEvaluator" + + +class JsonSimilarityEvaluator( + OutputEvaluator[ + OutputEvaluationCriteria, + JsonSimilarityEvaluatorConfig, + JsonSimilarityJustification, + ] +): + """Deterministic evaluator that scores structural JSON similarity between expected and actual output. + + Compares expected versus actual JSON-like structures and returns a + numerical score in the range [0, 100]. The comparison is token-based + and tolerant for numbers and strings (via Levenshtein distance). + """ + + @classmethod + def get_evaluator_id(cls) -> str: + """Get the evaluator id.""" + return EvaluatorType.JSON_SIMILARITY.value + + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: OutputEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate similarity between expected and actual JSON outputs. + + Uses token-based comparison with tolerance for numeric differences + and Levenshtein distance for string similarity. + + agent_execution: The execution details containing: + - agent_input: The input received by the agent + - actual_output: The actual output from the agent + - spans: The execution spans to use for the evaluation + evaluation_criteria: The criteria to evaluate + + Returns: + EvaluationResult: Numerical score between 0-100 indicating similarity + """ + expected_output = self._get_expected_output(evaluation_criteria) + actual_output = self._get_actual_output(agent_execution) + score, justification = self._compare_json( + expected_output, + actual_output, + ) + validated_justification = self.validate_justification(justification) + return NumericEvaluationResult( + score=score, + details=validated_justification, + ) + + def _compare_json(self, expected: Any, actual: Any) -> tuple[float, dict[str, Any]]: + matched_leaves, total_leaves = self._compare_tokens(expected, actual) + if total_leaves == 0: + sim = 1.0 + else: + sim = matched_leaves / total_leaves + return ( + max(0.0, min(1.0, sim)), + { + "expected": str(expected), + "actual": str(actual), + "matched_leaves": matched_leaves, + "total_leaves": total_leaves, + }, + ) + + def _compare_tokens( + self, expected_token: Any, actual_token: Any + ) -> Tuple[float, float]: + if self._is_number(expected_token) and self._is_number(actual_token): + return self._compare_numbers(float(expected_token), float(actual_token)) + + if type(expected_token) is not type(actual_token): + return 0.0, self._count_leaves(expected_token) + + if isinstance(expected_token, dict): + matched_leaves = total_leaves = 0.0 + # Only expected keys count + for expected_key, expected_value in expected_token.items(): + if isinstance(actual_token, dict) and expected_key in actual_token: + matched, total = self._compare_tokens( + expected_value, actual_token[expected_key] + ) + else: + matched, total = (0.0, self._count_leaves(expected_value)) + matched_leaves += matched + total_leaves += total + return matched_leaves, total_leaves + + if isinstance(expected_token, list): + matched_leaves = total_leaves = 0.0 + common_length = min(len(expected_token), len(actual_token)) + for index in range(common_length): + matched, total = self._compare_tokens( + expected_token[index], actual_token[index] + ) + matched_leaves += matched + total_leaves += total + for index in range(common_length, len(expected_token)): + total_leaves += self._count_leaves(expected_token[index]) + return (matched_leaves, total_leaves) + + if isinstance(expected_token, bool): + return (1.0, 1.0) if expected_token == actual_token else (0.0, 1.0) + + if isinstance(expected_token, str): + return self._compare_strings(expected_token, actual_token) + + return (1.0, 1.0) if str(expected_token) == str(actual_token) else (0.0, 1.0) + + def _compare_numbers( + self, expected_number: float, actual_number: float + ) -> Tuple[float, float]: + total = 1.0 + if math.isclose(expected_number, 0.0, abs_tol=1e-12): + matched = 1.0 if math.isclose(actual_number, 0.0, abs_tol=1e-12) else 0.0 + else: + ratio = abs(expected_number - actual_number) / abs(expected_number) + matched = max(0.0, min(1.0, 1.0 - ratio)) + return matched, total + + def _compare_strings( + self, expected_string: str, actual_string: str + ) -> Tuple[float, float]: + total = 1.0 + if not expected_string and not actual_string: + return 1.0, total + distance = self._levenshtein(expected_string, actual_string) + max_length = max(len(expected_string), len(actual_string)) + similarity = 1.0 - (distance / max_length) if max_length else 1.0 + similarity = max(0.0, min(1.0, similarity)) + return similarity, total + + def _count_leaves(self, token_node: Any) -> float: + if isinstance(token_node, dict): + return sum( + self._count_leaves(child_value) for child_value in token_node.values() + ) + if isinstance(token_node, list): + return sum(self._count_leaves(child_value) for child_value in token_node) + return 1.0 + + def _levenshtein(self, source_text: str, target_text: str) -> int: + if not source_text: + return len(target_text) + if not target_text: + return len(source_text) + source_len, target_len = len(source_text), len(target_text) + distance_matrix = [[0] * (target_len + 1) for _ in range(source_len + 1)] + for row_idx in range(source_len + 1): + distance_matrix[row_idx][0] = row_idx + for col_idx in range(target_len + 1): + distance_matrix[0][col_idx] = col_idx + for row_idx in range(1, source_len + 1): + for col_idx in range(1, target_len + 1): + substitution_cost = ( + 0 if source_text[row_idx - 1] == target_text[col_idx - 1] else 1 + ) + distance_matrix[row_idx][col_idx] = min( + distance_matrix[row_idx - 1][col_idx] + 1, # deletion + distance_matrix[row_idx][col_idx - 1] + 1, # insertion + distance_matrix[row_idx - 1][col_idx - 1] + + substitution_cost, # substitution + ) + return distance_matrix[source_len][target_len] + + def _is_number(self, value: Any) -> bool: + return isinstance(value, (int, float)) and not isinstance(value, bool) diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/legacy_deterministic_evaluator_base.py b/packages/uipath-eval/src/uipath_eval/evaluators/legacy_deterministic_evaluator_base.py new file mode 100644 index 000000000..faa5cac17 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/legacy_deterministic_evaluator_base.py @@ -0,0 +1,53 @@ +"""Base class for deterministic evaluators that provide consistent outputs.""" + +import json +from abc import ABC +from typing import Any, Generic, TypeVar + +from .base_legacy_evaluator import BaseLegacyEvaluator, LegacyEvaluatorConfig + +T = TypeVar("T", bound=LegacyEvaluatorConfig) + + +class BaseLegacyDeterministicEvaluator(BaseLegacyEvaluator[T], Generic[T], ABC): + """Base class for evaluators that produce deterministic, reproducible results. + + This class provides utility methods for canonical JSON comparison and number normalization + to ensure consistent evaluation results across runs. + """ + + def _canonical_json(self, obj: Any) -> str: + """Convert an object to canonical JSON string for consistent comparison. + + Args: + obj: The object to convert to canonical JSON + + Returns: + str: Canonical JSON string with normalized numbers and sorted keys + """ + return json.dumps( + self._normalize_numbers(obj), + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ) + + def _normalize_numbers(self, obj: Any) -> Any: + """Recursively normalize numbers in nested data structures. + + Converts all numeric values (int, float) to float for consistent comparison, + while preserving booleans and other data types. + + Args: + obj: The object to normalize + + Returns: + Any: Object with normalized numbers + """ + if isinstance(obj, dict): + return {k: self._normalize_numbers(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [self._normalize_numbers(v) for v in obj] + if isinstance(obj, (int, float)) and not isinstance(obj, bool): + return float(obj) + return obj diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/legacy_exact_match_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/legacy_exact_match_evaluator.py new file mode 100644 index 000000000..bfbbc55cd --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/legacy_exact_match_evaluator.py @@ -0,0 +1,72 @@ +"""Exact match evaluator for binary pass/fail evaluation of agent outputs.""" + +from uipath_eval.models import BooleanEvaluationResult, EvaluationResult + +from .._helpers.output_path import resolve_output_path +from ..models.models import AgentExecution +from .base_legacy_evaluator import LegacyEvaluationCriteria, LegacyEvaluatorConfig +from .legacy_deterministic_evaluator_base import BaseLegacyDeterministicEvaluator + + +class LegacyExactMatchEvaluatorConfig(LegacyEvaluatorConfig): + """Configuration for legacy exact-match evaluators.""" + + name: str = "LegacyExactMatchEvaluator" + + +class LegacyExactMatchEvaluator( + BaseLegacyDeterministicEvaluator[LegacyExactMatchEvaluatorConfig] +): + """Evaluator that performs exact structural matching between expected and actual outputs. + + This evaluator returns True if the actual output exactly matches the expected output + after canonical JSON normalization, and False otherwise. Numbers are normalized + to floats for consistent comparison. + """ + + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: LegacyEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate whether actual output exactly matches expected output. + + Args: + agent_execution: The execution details containing: + - agent_input: The input received by the agent + - actual_output: The actual output from the agent + - spans: The execution spans to use for the evaluation + evaluation_criteria: The criteria to evaluate + + Returns: + EvaluationResult: Boolean result indicating exact match (True/False) + """ + actual_output = agent_execution.agent_output + expected_output = evaluation_criteria.expected_output + + if self.target_output_key and self.target_output_key != "*": + if isinstance(actual_output, dict) and isinstance(expected_output, dict): + actual_resolved = True + expected_resolved = True + + try: + actual_output = resolve_output_path( + actual_output, self.target_output_key + ) + except (KeyError, IndexError, TypeError): + actual_resolved = False + + try: + expected_output = resolve_output_path( + expected_output, self.target_output_key + ) + except (KeyError, IndexError, TypeError): + expected_resolved = False + + if not actual_resolved or not expected_resolved: + actual_output = expected_output = {} + + return BooleanEvaluationResult( + score=self._canonical_json(actual_output) + == self._canonical_json(expected_output) + ) diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/legacy_json_similarity_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/legacy_json_similarity_evaluator.py new file mode 100644 index 000000000..70fe28b2e --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/legacy_json_similarity_evaluator.py @@ -0,0 +1,180 @@ +"""JSON similarity evaluator for flexible structural comparison of outputs.""" + +import math +from typing import Any, Tuple, TypeVar + +from .._helpers.output_path import resolve_output_path +from ..models import EvaluationResult, NumericEvaluationResult +from ..models.models import AgentExecution +from .base_legacy_evaluator import LegacyEvaluationCriteria, LegacyEvaluatorConfig +from .legacy_deterministic_evaluator_base import BaseLegacyDeterministicEvaluator + +T = TypeVar("T") + + +class LegacyJsonSimilarityEvaluatorConfig(LegacyEvaluatorConfig): + """Configuration for legacy json-similarity evaluators.""" + + name: str = "LegacyJsonSimilarityEvaluator" + + +class LegacyJsonSimilarityEvaluator( + BaseLegacyDeterministicEvaluator[LegacyJsonSimilarityEvaluatorConfig] +): + """Legacy deterministic evaluator that scores structural JSON similarity between expected and actual output. + + Compares expected versus actual JSON-like structures and returns a + numerical score in the range [0, 100]. The comparison is token-based + and tolerant for numbers and strings (via Levenshtein distance). + """ + + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: LegacyEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate similarity between expected and actual JSON outputs. + + Uses token-based comparison with tolerance for numeric differences + and Levenshtein distance for string similarity. + + agent_execution: The execution details containing: + - agent_input: The input received by the agent + - actual_output: The actual output from the agent + - spans: The execution spans to use for the evaluation + evaluation_criteria: The criteria to evaluate + + Returns: + EvaluationResult: Numerical score between 0-100 indicating similarity + """ + actual_output = agent_execution.agent_output + expected_output = evaluation_criteria.expected_output + + if self.target_output_key and self.target_output_key != "*": + try: + actual_output = resolve_output_path( + actual_output, self.target_output_key + ) + except (KeyError, IndexError, TypeError): + actual_output = {} + + try: + expected_output = resolve_output_path( + expected_output, self.target_output_key + ) + except (KeyError, IndexError, TypeError): + expected_output = {} + + return NumericEvaluationResult( + score=self._compare_json(expected_output, actual_output) + ) + + def _compare_json(self, expected: Any, actual: Any) -> float: + matched_leaves, total_leaves = self._compare_tokens(expected, actual) + if total_leaves == 0: + return 100.0 + sim = (matched_leaves / total_leaves) * 100.0 + return max(0.0, min(100.0, sim)) + + def _compare_tokens( + self, expected_token: Any, actual_token: Any + ) -> Tuple[float, float]: + if self._is_number(expected_token) and self._is_number(actual_token): + return self._compare_numbers(float(expected_token), float(actual_token)) + + if type(expected_token) is not type(actual_token): + return 0.0, self._count_leaves(expected_token) + + if isinstance(expected_token, dict): + matched_leaves = total_leaves = 0.0 + # Only expected keys count + for expected_key, expected_value in expected_token.items(): + if isinstance(actual_token, dict) and expected_key in actual_token: + matched, total = self._compare_tokens( + expected_value, actual_token[expected_key] + ) + else: + matched, total = (0.0, self._count_leaves(expected_value)) + matched_leaves += matched + total_leaves += total + return matched_leaves, total_leaves + + if isinstance(expected_token, list): + matched_leaves = total_leaves = 0.0 + common_length = min(len(expected_token), len(actual_token)) + for index in range(common_length): + matched, total = self._compare_tokens( + expected_token[index], actual_token[index] + ) + matched_leaves += matched + total_leaves += total + for index in range(common_length, len(expected_token)): + total_leaves += self._count_leaves(expected_token[index]) + return (matched_leaves, total_leaves) + + if isinstance(expected_token, bool): + return (1.0, 1.0) if expected_token == actual_token else (0.0, 1.0) + + if isinstance(expected_token, str): + return self._compare_strings(expected_token, actual_token) + + return (1.0, 1.0) if str(expected_token) == str(actual_token) else (0.0, 1.0) + + def _compare_numbers( + self, expected_number: float, actual_number: float + ) -> Tuple[float, float]: + total = 1.0 + if math.isclose(expected_number, 0.0, abs_tol=1e-12): + matched = 1.0 if math.isclose(actual_number, 0.0, abs_tol=1e-12) else 0.0 + else: + ratio = abs(expected_number - actual_number) / abs(expected_number) + matched = max(0.0, min(1.0, 1.0 - ratio)) + return matched, total + + def _compare_strings( + self, expected_string: str, actual_string: str + ) -> Tuple[float, float]: + total = 1.0 + if not expected_string and not actual_string: + return 1.0, total + distance = self._levenshtein(expected_string, actual_string) + max_length = max(len(expected_string), len(actual_string)) + similarity = 1.0 - (distance / max_length) if max_length else 1.0 + similarity = max(0.0, min(1.0, similarity)) + return similarity, total + + def _count_leaves(self, token_node: Any) -> float: + if isinstance(token_node, dict): + return sum( + self._count_leaves(child_value) for child_value in token_node.values() + ) + if isinstance(token_node, list): + return sum(self._count_leaves(child_value) for child_value in token_node) + return 1.0 + + def _levenshtein(self, source_text: str, target_text: str) -> int: + if not source_text: + return len(target_text) + if not target_text: + return len(source_text) + source_len, target_len = len(source_text), len(target_text) + distance_matrix = [[0] * (target_len + 1) for _ in range(source_len + 1)] + for row_idx in range(source_len + 1): + distance_matrix[row_idx][0] = row_idx + for col_idx in range(target_len + 1): + distance_matrix[0][col_idx] = col_idx + for row_idx in range(1, source_len + 1): + for col_idx in range(1, target_len + 1): + substitution_cost = ( + 0 if source_text[row_idx - 1] == target_text[col_idx - 1] else 1 + ) + distance_matrix[row_idx][col_idx] = min( + distance_matrix[row_idx - 1][col_idx] + 1, # deletion + distance_matrix[row_idx][col_idx - 1] + 1, # insertion + distance_matrix[row_idx - 1][col_idx - 1] + + substitution_cost, # substitution + ) + return distance_matrix[source_len][target_len] + + def _is_number(self, value: Any) -> bool: + return isinstance(value, (int, float)) and not isinstance(value, bool) diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/multiclass_classification_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/multiclass_classification_evaluator.py new file mode 100644 index 000000000..69790c3aa --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/multiclass_classification_evaluator.py @@ -0,0 +1,198 @@ +"""Multiclass classification evaluator for agent outputs. + +Evaluates multiclass classification by comparing predicted vs expected class. +Per-datapoint score is 1.0 (correct) or 0.0 (incorrect). The reduce_scores +method reads predicted/expected from justification details to reconstruct a +confusion matrix and compute precision, recall, or F-score with micro or +macro averaging. +""" + +from typing import Literal + +from ..models import ( + AgentExecution, + EvaluationResult, + EvaluatorType, + NumericEvaluationResult, +) +from ..models.models import ( + EvaluationResultDto, + UiPathEvaluationError, + UiPathEvaluationErrorCategory, +) +from .base_evaluator import BaseEvaluationCriteria, BaseEvaluatorJustification +from .output_evaluator import ( + BaseOutputEvaluator, + OutputEvaluatorConfig, +) + + +class MulticlassClassificationEvaluationCriteria(BaseEvaluationCriteria): + """Per-datapoint criteria: which class this sample should belong to.""" + + expected_class: str + + +class MulticlassClassificationEvaluatorConfig( + OutputEvaluatorConfig[MulticlassClassificationEvaluationCriteria] +): + """Configuration for the multiclass classification evaluator.""" + + name: str = "MulticlassClassificationEvaluator" + classes: list[str] + metric_type: Literal["precision", "recall", "f-score"] = "f-score" + averaging: Literal["micro", "macro"] = "macro" + f_value: float = 1.0 + + +class MulticlassClassificationEvaluator( + BaseOutputEvaluator[ + MulticlassClassificationEvaluationCriteria, + MulticlassClassificationEvaluatorConfig, + BaseEvaluatorJustification, + ] +): + """Multiclass classification evaluator with micro/macro averaging. + + Per-datapoint scores are 1.0 (correct) or 0.0 (incorrect). The reduce_scores + method reads predicted/expected from justification details to reconstruct the + confusion matrix and compute the configured metric. + """ + + @classmethod + def get_evaluator_id(cls) -> str: + """Get the evaluator id.""" + return EvaluatorType.MULTICLASS_CLASSIFICATION.value + + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: MulticlassClassificationEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate multiclass classification by comparing predicted vs expected class.""" + predicted_class = str(self._get_actual_output(agent_execution)).lower() + expected_class = evaluation_criteria.expected_class.lower() + classes = [c.lower() for c in self.evaluator_config.classes] + + if expected_class not in classes: + raise UiPathEvaluationError( + code="INVALID_EXPECTED_CLASS", + title="Expected class not in configured classes", + detail=f"Expected class '{expected_class}' is not in the configured classes: {classes}", + category=UiPathEvaluationErrorCategory.USER, + ) + + if predicted_class not in classes: + raise UiPathEvaluationError( + code="INVALID_PREDICTED_CLASS", + title="Predicted class not in configured classes", + detail=f"Predicted class '{predicted_class}' is not in the configured classes: {classes}", + category=UiPathEvaluationErrorCategory.USER, + ) + + score = 1.0 if predicted_class == expected_class else 0.0 + + justification = self.validate_justification( + { + "expected": expected_class, + "actual": predicted_class, + } + ) + return NumericEvaluationResult(score=score, details=justification) + + def reduce_scores(self, results: list[EvaluationResultDto]) -> float: + """Reconstruct confusion matrix from details and compute the configured metric.""" + if not results: + return 0.0 + + classes = [c.lower() for c in self.evaluator_config.classes] + k = len(classes) + metric_type = self.evaluator_config.metric_type + averaging = self.evaluator_config.averaging + f_value = self.evaluator_config.f_value + + # Reconstruct confusion matrix: confusion[pred_idx][exp_idx] + confusion = [[0] * k for _ in range(k)] + for r in results: + if isinstance(r.details, BaseEvaluatorJustification): + details = r.details + elif isinstance(r.details, dict): + try: + details = BaseEvaluatorJustification.model_validate(r.details) + except Exception: + continue + else: + continue + pred = details.actual + exp = details.expected + if pred in classes and exp in classes: + confusion[classes.index(pred)][classes.index(exp)] += 1 + + if averaging == "micro": + return _micro_metric(confusion, k, metric_type, f_value) + else: + return _macro_metric(confusion, k, metric_type, f_value) + + +def _micro_metric( + confusion: list[list[int]], + k: int, + metric_type: str, + f_value: float, +) -> float: + """Compute micro-averaged metric from confusion matrix.""" + total_tp = sum(confusion[i][i] for i in range(k)) + # For micro-averaging, sum TP/FP/FN across all classes + total_fp = sum( + sum(confusion[i][j] for j in range(k)) - confusion[i][i] for i in range(k) + ) + total_fn = sum( + sum(confusion[j][i] for j in range(k)) - confusion[i][i] for i in range(k) + ) + + if metric_type == "precision": + denom = total_tp + total_fp + return total_tp / denom if denom > 0 else 0.0 + elif metric_type == "recall": + denom = total_tp + total_fn + return total_tp / denom if denom > 0 else 0.0 + elif metric_type == "f-score": + p = total_tp / (total_tp + total_fp) if (total_tp + total_fp) > 0 else 0.0 + rec = total_tp / (total_tp + total_fn) if (total_tp + total_fn) > 0 else 0.0 + beta_sq = f_value**2 + f_denom = beta_sq * p + rec + return (1 + beta_sq) * p * rec / f_denom if f_denom > 0 else 0.0 + return 0.0 + + +def _macro_metric( + confusion: list[list[int]], + k: int, + metric_type: str, + f_value: float, +) -> float: + """Compute macro-averaged metric from confusion matrix.""" + per_class_metrics: list[float] = [] + + for c in range(k): + tp = confusion[c][c] + fp = sum(confusion[c][j] for j in range(k)) - tp + fn = sum(confusion[j][c] for j in range(k)) - tp + + if metric_type == "precision": + denom = tp + fp + per_class_metrics.append(tp / denom if denom > 0 else 0.0) + elif metric_type == "recall": + denom = tp + fn + per_class_metrics.append(tp / denom if denom > 0 else 0.0) + elif metric_type == "f-score": + p = tp / (tp + fp) if (tp + fp) > 0 else 0.0 + rec = tp / (tp + fn) if (tp + fn) > 0 else 0.0 + beta_sq = f_value**2 + f_denom = beta_sq * p + rec + f_score = (1 + beta_sq) * p * rec / f_denom if f_denom > 0 else 0.0 + per_class_metrics.append(f_score) + + if not per_class_metrics: + return 0.0 + return sum(per_class_metrics) / len(per_class_metrics) diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/output_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/output_evaluator.py new file mode 100644 index 000000000..1dde550ac --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/output_evaluator.py @@ -0,0 +1,138 @@ +"""Base class for all output evaluator configurations.""" + +import json +from typing import Any, TypeVar, Union + +from pydantic import Field + +from .._helpers.output_path import resolve_output_path +from ..models import AgentExecution +from ..models.models import UiPathEvaluationError, UiPathEvaluationErrorCategory +from .base_evaluator import ( + BaseEvaluationCriteria, + BaseEvaluator, + BaseEvaluatorConfig, + BaseEvaluatorJustification, +) + + +class OutputEvaluationCriteria(BaseEvaluationCriteria): + """Base class for all output evaluation criteria.""" + + expected_output: dict[str, Any] | str = Field(..., alias="expectedOutput") + + +T = TypeVar("T", bound=BaseEvaluationCriteria) +T_OutputCriteria = TypeVar("T_OutputCriteria", bound=OutputEvaluationCriteria) + + +class OutputEvaluatorConfig(BaseEvaluatorConfig[T]): + """Base class for all output evaluator configurations. + + Generic over T to allow subclasses to define their own + specific output evaluation criteria types while maintaining type safety. + """ + + target_output_key: str = Field( + default="*", description="Key to extract output from agent execution" + ) + + +C = TypeVar("C", bound=OutputEvaluatorConfig[Any]) +J = TypeVar("J", bound=Union[str, BaseEvaluatorJustification]) + + +# NOTE: This evaluator is only used in coded evaluators +class BaseOutputEvaluator(BaseEvaluator[T, C, J]): + """Abstract base class for all output evaluators. + + Generic Parameters: + T_OutputCriteria: The output evaluation criteria type + C: The output evaluator config type (bound to OutputEvaluatorConfig[T_OutputCriteria]) + J: The justification type + """ + + def _normalize_numbers(self, obj: Any) -> Any: + """Recursively normalize int/float to float for consistent numeric comparison. + + Converts all numeric values (int, float) to float in nested structures + (dicts, lists), while preserving booleans and other data types. + """ + if isinstance(obj, dict): + return {k: self._normalize_numbers(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [self._normalize_numbers(v) for v in obj] + if isinstance(obj, (int, float)) and not isinstance(obj, bool): + return float(obj) + return obj + + def _get_actual_output(self, agent_execution: AgentExecution) -> Any: + """Get the actual output from the agent execution.""" + if self.evaluator_config.target_output_key != "*": + try: + result = resolve_output_path( + agent_execution.agent_output, + self.evaluator_config.target_output_key, + ) + except (KeyError, IndexError, TypeError) as e: + raise UiPathEvaluationError( + code="TARGET_OUTPUT_KEY_NOT_FOUND", + title="Target output key not found in actual output", + detail=f"Error: {e}", + category=UiPathEvaluationErrorCategory.USER, + ) from e + else: + result = agent_execution.agent_output + return self._normalize_numbers(result) + + def _get_full_expected_output(self, evaluation_criteria: T) -> Any: + """Get the full expected output from the evaluation criteria.""" + raise UiPathEvaluationError( + code="NOT_IMPLEMENTED", + title="This method was not implemented by the subclass.", + detail="This method was not implemented by the subclass.", + category=UiPathEvaluationErrorCategory.SYSTEM, + ) + + def _get_expected_output(self, evaluation_criteria: T) -> Any: + """Load the expected output from the evaluation criteria.""" + expected_output = self._get_full_expected_output(evaluation_criteria) + if self.evaluator_config.target_output_key != "*": + if isinstance(expected_output, str): + try: + expected_output = json.loads(expected_output) + except json.JSONDecodeError as e: + raise UiPathEvaluationError( + code="INVALID_EXPECTED_OUTPUT", + title="When target output key is not '*', expected output must be a dictionary or a valid JSON string", + detail=f"Error: {e}", + category=UiPathEvaluationErrorCategory.USER, + ) from e + try: + expected_output = resolve_output_path( + expected_output, + self.evaluator_config.target_output_key, + ) + except (KeyError, IndexError, TypeError) as e: + raise UiPathEvaluationError( + code="TARGET_OUTPUT_KEY_NOT_FOUND", + title="Target output key not found in expected output", + detail=f"Error: {e}", + category=UiPathEvaluationErrorCategory.USER, + ) from e + return self._normalize_numbers(expected_output) + + +# NOTE: This evaluator is only used in coded evaluators. +class OutputEvaluator(BaseOutputEvaluator[T_OutputCriteria, C, J]): + """Abstract base class for all output evaluators. + + Generic Parameters: + T_OutputCriteria: The output evaluation criteria type + C: The output evaluator config type (bound to OutputEvaluatorConfig[T_OutputCriteria]) + J: The justification type + """ + + def _get_full_expected_output(self, evaluation_criteria: T_OutputCriteria) -> Any: + """Get the full expected output from the evaluation criteria.""" + return evaluation_criteria.expected_output diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/registration.py b/packages/uipath-eval/src/uipath_eval/evaluators/registration.py new file mode 100644 index 000000000..82690f255 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/registration.py @@ -0,0 +1,188 @@ +"""Module for registering custom evaluators in the UiPath evaluation framework.""" + +import ast +import importlib.util +import json +import logging +import re +import sys +from pathlib import Path +from typing import Any + +from ..constants import CUSTOM_EVALUATOR_PREFIX, EVALS_FOLDER + +logger = logging.getLogger(__name__) + + +class EvaluatorRegistrationError(Exception): + """Raised when evaluator registration fails.""" + + pass + + +def to_kebab_case(text: str) -> str: + """Convert a given string to kebab-case.""" + return re.sub(r"(? Path | None: + """Find the evaluator file in evals/evaluators/custom folder.""" + custom_evaluators_path = Path.cwd() / EVALS_FOLDER / "evaluators" / "custom" + + if not custom_evaluators_path.exists(): + return None + + file_path = custom_evaluators_path / filename + if file_path.exists(): + return file_path + + return None + + +def find_base_evaluator_class(file_path: Path) -> str | None: + """Parse the Python file and find the class that inherits from BaseEvaluator.""" + try: + with open(file_path, "r") as f: + tree = ast.parse(f.read(), filename=str(file_path)) + + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + for base in node.bases: + if isinstance(base, ast.Name) and base.id == "BaseEvaluator": + return node.name + elif isinstance(base, ast.Subscript): + if ( + isinstance(base.value, ast.Name) + and base.value.id == "BaseEvaluator" + ): + return node.name + + return None + except Exception as e: + logger.error(f"Error parsing file: {e}") + return None + + +def load_evaluator_class(file_path: Path, class_name: str) -> Any | None: + """Dynamically load the evaluator class from the file.""" + parent_dir: str | None = None + try: + parent_dir = str(file_path.parent) + if parent_dir not in sys.path: + sys.path.insert(0, parent_dir) + + spec = importlib.util.spec_from_file_location("custom_evaluator", file_path) + if spec is None or spec.loader is None: + return None + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + if hasattr(module, class_name): + return getattr(module, class_name) + + return None + except Exception as e: + logger.error(f"Error loading class: {e}") + return None + finally: + # Remove from sys.path + if parent_dir is not None and parent_dir in sys.path: + sys.path.remove(parent_dir) + + +def generate_evaluator_config(evaluator_class: Any, class_name: str) -> dict[str, Any]: + """Generate the evaluator config from the class.""" + try: + config_type = evaluator_class._extract_config_type() + config_instance = config_type() + config_dict = config_instance.model_dump(by_alias=True, exclude_none=False) + + return config_dict + except Exception as e: + raise EvaluatorRegistrationError( + f"Error inferring evaluator config: {e}" + ) from e + + +def register_evaluator(filename: str) -> tuple[str, str]: + """Infers the schema and types of a custom evaluator. + + Returns: + tuple[str, str]: + - The first string is the path to the python evaluator file. + - The second string is the evaluator type that corresponds to the schema file. + + Raises: + EvaluatorRegistrationError: If registration fails at any step. + """ + if not filename.endswith(".py"): + filename = filename + ".py" + file_path = find_evaluator_file(filename) + if file_path is None: + raise EvaluatorRegistrationError( + f"Could not find '{filename}' in {EVALS_FOLDER}/evaluators/custom folder" + ) + + logger.info(f"Found custom evaluator file: evals/evaluators/custom/{filename}") + + class_name = find_base_evaluator_class(file_path) + if class_name is None: + raise EvaluatorRegistrationError( + f"Could not find a class inheriting from BaseEvaluator in {filename}" + ) + + logger.info(f"Found custom evaluator class: {class_name}") + + evaluator_class = load_evaluator_class(file_path, class_name) + if evaluator_class is None: + raise EvaluatorRegistrationError( + f"Could not load class {class_name} from {filename}" + ) + + try: + evaluator_id = evaluator_class.get_evaluator_id() + except Exception as e: + raise EvaluatorRegistrationError(f"Error getting evaluator ID: {e}") from e + + evaluator_config = generate_evaluator_config(evaluator_class, class_name) + evaluator_json_type = evaluator_class.generate_json_type() + + evaluators_dir = Path.cwd() / EVALS_FOLDER / "evaluators" + evaluators_dir.mkdir(parents=True, exist_ok=True) + + evaluator_types_dir = evaluators_dir / "custom" / "types" + evaluator_types_dir.mkdir(parents=True, exist_ok=True) + + kebab_class_name = to_kebab_case(class_name) + output_file_evaluator_types = kebab_class_name + "-types.json" + evaluator_types_output_path = ( + evaluators_dir / "custom" / "types" / output_file_evaluator_types + ) + + with open(evaluator_types_output_path, "w") as f: + json.dump(evaluator_json_type, f, indent=2) + + logger.info( + f"Generated evaluator types: {EVALS_FOLDER}/evaluators/custom/types/{output_file_evaluator_types}" + ) + + output = { + "version": "1.0", + "id": evaluator_id, + "evaluatorTypeId": f"{CUSTOM_EVALUATOR_PREFIX}types/{output_file_evaluator_types}", + "evaluatorSchema": f"{CUSTOM_EVALUATOR_PREFIX}{filename}:{class_name}", + "description": evaluator_class.__doc__, + "evaluatorConfig": evaluator_config, + } + + output_file_evaluator_spec = kebab_class_name + ".json" + evaluator_spec_output_path = evaluators_dir / output_file_evaluator_spec + with open(evaluator_spec_output_path, "w") as f: + json.dump(output, f, indent=2) + + logger.info( + f"Generated evaluator spec: evals/evaluators/{output_file_evaluator_spec}" + ) + + return str(file_path), str(evaluator_types_output_path) diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_args_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_args_evaluator.py new file mode 100644 index 000000000..2703e3c76 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_args_evaluator.py @@ -0,0 +1,82 @@ +"""Tool call order evaluator for validating correct sequence of tool calls.""" + +from .._helpers.evaluators_helpers import ( + extract_tool_calls, + tool_calls_args_score, +) +from ..models import AgentExecution, EvaluationResult, NumericEvaluationResult, ToolCall +from ..models.models import EvaluatorType +from .base_evaluator import ( + BaseEvaluationCriteria, + BaseEvaluator, + BaseEvaluatorConfig, + BaseEvaluatorJustification, +) + + +class ToolCallArgsEvaluationCriteria(BaseEvaluationCriteria): + """Evaluation criteria for the tool call order evaluator.""" + + # TODO: name field of ToolCall needs to be validated such that it contains only the tools available + tool_calls: list[ToolCall] + + +class ToolCallArgsEvaluatorConfig(BaseEvaluatorConfig[ToolCallArgsEvaluationCriteria]): + """Configuration for the tool call count evaluator.""" + + name: str = "ToolCallArgsEvaluator" + strict: bool = False + subset: bool = False + + +class ToolCallArgsEvaluatorJustification(BaseEvaluatorJustification): + """Justification for the tool call args evaluator.""" + + explained_tool_calls_args: dict[str, str] + + +class ToolCallArgsEvaluator( + BaseEvaluator[ + ToolCallArgsEvaluationCriteria, + ToolCallArgsEvaluatorConfig, + ToolCallArgsEvaluatorJustification, + ] +): + """Evaluator that checks if the tool calls are in the correct order. + + This evaluator returns True if the tool calls are in the correct order, and False otherwise. + """ + + @classmethod + def get_evaluator_id(cls) -> str: + """Get the evaluator id.""" + return EvaluatorType.TOOL_CALL_ARGS.value + + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: ToolCallArgsEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate if the tool calls are in the correct order. + + Args: + agent_execution: The execution details containing: + - agent_input: The input received by the agent + - agent_output: The final output of the agent + - agent_trace: The execution spans to use for the evaluation + evaluation_criteria: The criteria to evaluate + Returns: + EvaluationResult: Boolean result indicating correct tool call order (True/False) + """ + tool_calls_order = extract_tool_calls(agent_execution.agent_trace) + score, justification = tool_calls_args_score( + tool_calls_order, + evaluation_criteria.tool_calls, + self.evaluator_config.strict, + self.evaluator_config.subset, + ) + validated_justification = self.validate_justification(justification) + return NumericEvaluationResult( + score=score, + details=validated_justification, + ) diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_count_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_count_evaluator.py new file mode 100644 index 000000000..11d684ae1 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_count_evaluator.py @@ -0,0 +1,87 @@ +"""Tool call count evaluator for validating expected tool usage patterns.""" + +from collections import Counter + +from .._helpers.evaluators_helpers import ( + extract_tool_calls_names, + tool_calls_count_score, +) +from ..models import AgentExecution, EvaluationResult, NumericEvaluationResult +from ..models.models import EvaluatorType +from .base_evaluator import ( + BaseEvaluationCriteria, + BaseEvaluator, + BaseEvaluatorConfig, + BaseEvaluatorJustification, +) + + +class ToolCallCountEvaluationCriteria(BaseEvaluationCriteria): + """Evaluation criteria for the tool call count evaluator.""" + + # TODO: str field needs to be validated against some criteria that allows ">x", "=x", "<=x", "x" + tool_calls_count: dict[str, tuple[str, int]] + + +class ToolCallCountEvaluatorConfig( + BaseEvaluatorConfig[ToolCallCountEvaluationCriteria] +): + """Configuration for the tool call count evaluator.""" + + name: str = "ToolCallCountEvaluator" + strict: bool = False + + +class ToolCallCountEvaluatorJustification(BaseEvaluatorJustification): + """Justification for the tool call count evaluator.""" + + explained_tool_calls_count: dict[str, str] + + +class ToolCallCountEvaluator( + BaseEvaluator[ + ToolCallCountEvaluationCriteria, + ToolCallCountEvaluatorConfig, + ToolCallCountEvaluatorJustification, + ] +): + """Evaluator that checks if the tool calls match the expected count. + + This evaluator returns a score based on how well the actual tool call counts + match the expected counts specified in the criteria. + """ + + @classmethod + def get_evaluator_id(cls) -> str: + """Get the evaluator id.""" + return EvaluatorType.TOOL_CALL_COUNT.value + + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: ToolCallCountEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate if the tool calls are in the correct order. + + Args: + agent_execution: The execution details containing: + - agent_input: The input received by the agent + - agent_output: The final output of the agent + - agent_trace: The execution spans to use for the evaluation + evaluation_criteria: The criteria to evaluate + Returns: + EvaluationResult: Boolean result indicating correct tool call order (True/False) + """ + tool_calls_count = Counter( + extract_tool_calls_names(agent_execution.agent_trace) + ) + score, justification = tool_calls_count_score( + tool_calls_count, + evaluation_criteria.tool_calls_count, + self.evaluator_config.strict, + ) + validated_justification = self.validate_justification(justification) + return NumericEvaluationResult( + score=score, + details=validated_justification, + ) diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_order_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_order_evaluator.py new file mode 100644 index 000000000..1050ddc76 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_order_evaluator.py @@ -0,0 +1,82 @@ +"""Tool call order evaluator for validating correct sequence of tool calls.""" + +from .._helpers.evaluators_helpers import ( + extract_tool_calls_names, + tool_calls_order_score, +) +from ..models import AgentExecution, EvaluationResult, NumericEvaluationResult +from ..models.models import EvaluatorType +from .base_evaluator import ( + BaseEvaluationCriteria, + BaseEvaluator, + BaseEvaluatorConfig, + BaseEvaluatorJustification, +) + + +class ToolCallOrderEvaluationCriteria(BaseEvaluationCriteria): + """Evaluation criteria for the tool call order evaluator.""" + + # TODO: str field needs to be validated such that it contains only the tools available + tool_calls_order: list[str] + + +class ToolCallOrderEvaluatorConfig( + BaseEvaluatorConfig[ToolCallOrderEvaluationCriteria] +): + """Configuration for the tool call count evaluator.""" + + name: str = "ToolCallOrderEvaluator" + strict: bool = False + + +class ToolCallOrderEvaluatorJustification(BaseEvaluatorJustification): + """Justification for the tool call order evaluator.""" + + lcs: list[str] + + +class ToolCallOrderEvaluator( + BaseEvaluator[ + ToolCallOrderEvaluationCriteria, + ToolCallOrderEvaluatorConfig, + ToolCallOrderEvaluatorJustification, + ] +): + """Evaluator that checks if the tool calls are in the correct order. + + This evaluator returns True if the tool calls are in the correct order, and False otherwise. + """ + + @classmethod + def get_evaluator_id(cls) -> str: + """Get the evaluator id.""" + return EvaluatorType.TOOL_CALL_ORDER.value + + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: ToolCallOrderEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate if the tool calls are in the correct order. + + Args: + agent_execution: The execution details containing: + - agent_input: The input received by the agent + - agent_output: The final output of the agent + - agent_trace: The execution spans to use for the evaluation + evaluation_criteria: The criteria to evaluate + Returns: + EvaluationResult: Boolean result indicating correct tool call order (True/False) + """ + tool_calls_order = extract_tool_calls_names(agent_execution.agent_trace) + score, justification = tool_calls_order_score( + tool_calls_order, + evaluation_criteria.tool_calls_order, + self.evaluator_config.strict, + ) + validated_justification = self.validate_justification(justification) + return NumericEvaluationResult( + score=score, + details=validated_justification, + ) diff --git a/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_output_evaluator.py b/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_output_evaluator.py new file mode 100644 index 000000000..fff139daf --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators/tool_call_output_evaluator.py @@ -0,0 +1,87 @@ +"""Tool call order evaluator for validating correct sequence of tool calls.""" + +from .._helpers.evaluators_helpers import ( + extract_tool_calls_outputs, + tool_calls_output_score, +) +from ..models import ( + AgentExecution, + EvaluationResult, + NumericEvaluationResult, + ToolOutput, +) +from ..models.models import EvaluatorType +from .base_evaluator import ( + BaseEvaluationCriteria, + BaseEvaluator, + BaseEvaluatorConfig, + BaseEvaluatorJustification, +) + + +class ToolCallOutputEvaluationCriteria(BaseEvaluationCriteria): + """Evaluation criteria for the tool call order evaluator.""" + + # TODO: name field of ToolCall needs to be validated such that it contains only the tools available + tool_outputs: list[ToolOutput] + + +class ToolCallOutputEvaluatorConfig( + BaseEvaluatorConfig[ToolCallOutputEvaluationCriteria] +): + """Configuration for the tool call count evaluator.""" + + name: str = "ToolCallOutputEvaluator" + strict: bool = False + + +class ToolCallOutputEvaluatorJustification(BaseEvaluatorJustification): + """Justification for the tool call output evaluator.""" + + explained_tool_calls_outputs: dict[str, str] + + +class ToolCallOutputEvaluator( + BaseEvaluator[ + ToolCallOutputEvaluationCriteria, + ToolCallOutputEvaluatorConfig, + ToolCallOutputEvaluatorJustification, + ] +): + """Evaluator that checks if the tool calls are in the correct order. + + This evaluator returns True if the tool calls are in the correct order, and False otherwise. + """ + + @classmethod + def get_evaluator_id(cls) -> str: + """Get the evaluator id.""" + return EvaluatorType.TOOL_CALL_OUTPUT.value + + async def evaluate( + self, + agent_execution: AgentExecution, + evaluation_criteria: ToolCallOutputEvaluationCriteria, + ) -> EvaluationResult: + """Evaluate if the tool calls are in the correct order. + + Args: + agent_execution: The execution details containing: + - agent_input: The input received by the agent + - agent_output: The final output of the agent + - agent_trace: The execution spans to use for the evaluation + evaluation_criteria: The criteria to evaluate + Returns: + EvaluationResult: Boolean result indicating correct tool call order (True/False) + """ + tool_calls_outputs = extract_tool_calls_outputs(agent_execution.agent_trace) + score, justification = tool_calls_output_score( + tool_calls_outputs, + evaluation_criteria.tool_outputs, + self.evaluator_config.strict, + ) + validated_justification = self.validate_justification(justification) + return NumericEvaluationResult( + score=score, + details=validated_justification, + ) diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/ContainsEvaluator.json b/packages/uipath-eval/src/uipath_eval/evaluators_types/ContainsEvaluator.json new file mode 100644 index 000000000..9db709f59 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/ContainsEvaluator.json @@ -0,0 +1,79 @@ +{ + "evaluatorTypeId": "uipath-contains", + "evaluatorConfigSchema": { + "$defs": { + "ContainsEvaluationCriteria": { + "description": "Evaluation criteria for the contains evaluator.", + "properties": { + "search_text": { + "title": "Search Text", + "type": "string" + } + }, + "required": [ + "search_text" + ], + "title": "ContainsEvaluationCriteria", + "type": "object" + } + }, + "description": "Configuration for the contains evaluator.", + "properties": { + "name": { + "default": "ContainsEvaluator", + "title": "Name", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the evaluator", + "title": "Description", + "type": "string" + }, + "default_evaluation_criteria": { + "anyOf": [ + { + "$ref": "#/$defs/ContainsEvaluationCriteria" + }, + { + "type": "null" + } + ], + "default": null + }, + "target_output_key": { + "default": "*", + "description": "Key to extract output from agent execution", + "title": "Target Output Key", + "type": "string" + }, + "case_sensitive": { + "default": false, + "title": "Case Sensitive", + "type": "boolean" + }, + "negated": { + "default": false, + "title": "Negated", + "type": "boolean" + } + }, + "title": "ContainsEvaluatorConfig", + "type": "object" + }, + "evaluationCriteriaSchema": { + "description": "Evaluation criteria for the contains evaluator.", + "properties": { + "search_text": { + "title": "Search Text", + "type": "string" + } + }, + "required": [ + "search_text" + ], + "title": "ContainsEvaluationCriteria", + "type": "object" + }, + "justificationSchema": {} +} \ No newline at end of file diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/ExactMatchEvaluator.json b/packages/uipath-eval/src/uipath_eval/evaluators_types/ExactMatchEvaluator.json new file mode 100644 index 000000000..866b06416 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/ExactMatchEvaluator.json @@ -0,0 +1,95 @@ +{ + "evaluatorTypeId": "uipath-exact-match", + "evaluatorConfigSchema": { + "$defs": { + "OutputEvaluationCriteria": { + "description": "Base class for all output evaluation criteria.", + "properties": { + "expected_output": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "string" + } + ], + "title": "Expected Output" + } + }, + "required": [ + "expected_output" + ], + "title": "OutputEvaluationCriteria", + "type": "object" + } + }, + "description": "Configuration for the exact match evaluator.", + "properties": { + "name": { + "default": "ExactMatchEvaluator", + "title": "Name", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the evaluator", + "title": "Description", + "type": "string" + }, + "default_evaluation_criteria": { + "anyOf": [ + { + "$ref": "#/$defs/OutputEvaluationCriteria" + }, + { + "type": "null" + } + ], + "default": null + }, + "target_output_key": { + "default": "*", + "description": "Key to extract output from agent execution", + "title": "Target Output Key", + "type": "string" + }, + "case_sensitive": { + "default": false, + "title": "Case Sensitive", + "type": "boolean" + }, + "negated": { + "default": false, + "title": "Negated", + "type": "boolean" + } + }, + "title": "ExactMatchEvaluatorConfig", + "type": "object" + }, + "evaluationCriteriaSchema": { + "description": "Base class for all output evaluation criteria.", + "properties": { + "expected_output": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "string" + } + ], + "title": "Expected Output" + } + }, + "required": [ + "expected_output" + ], + "title": "OutputEvaluationCriteria", + "type": "object" + }, + "justificationSchema": {} +} \ No newline at end of file diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/JsonSimilarityEvaluator.json b/packages/uipath-eval/src/uipath_eval/evaluators_types/JsonSimilarityEvaluator.json new file mode 100644 index 000000000..ef17bf083 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/JsonSimilarityEvaluator.json @@ -0,0 +1,87 @@ +{ + "evaluatorTypeId": "uipath-json-similarity", + "evaluatorConfigSchema": { + "$defs": { + "OutputEvaluationCriteria": { + "description": "Base class for all output evaluation criteria.", + "properties": { + "expected_output": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "string" + } + ], + "title": "Expected Output" + } + }, + "required": [ + "expected_output" + ], + "title": "OutputEvaluationCriteria", + "type": "object" + } + }, + "description": "Configuration for the json similarity evaluator.", + "properties": { + "name": { + "default": "JsonSimilarityEvaluator", + "title": "Name", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the evaluator", + "title": "Description", + "type": "string" + }, + "default_evaluation_criteria": { + "anyOf": [ + { + "$ref": "#/$defs/OutputEvaluationCriteria" + }, + { + "type": "null" + } + ], + "default": null + }, + "target_output_key": { + "default": "*", + "description": "Key to extract output from agent execution", + "title": "Target Output Key", + "type": "string" + } + }, + "title": "JsonSimilarityEvaluatorConfig", + "type": "object" + }, + "evaluationCriteriaSchema": { + "description": "Base class for all output evaluation criteria.", + "properties": { + "expected_output": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "string" + } + ], + "title": "Expected Output" + } + }, + "required": [ + "expected_output" + ], + "title": "OutputEvaluationCriteria", + "type": "object" + }, + "justificationSchema": { + "type": "string" + } +} \ No newline at end of file diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeOutputEvaluator.json b/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeOutputEvaluator.json new file mode 100644 index 000000000..06f731c1f --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeOutputEvaluator.json @@ -0,0 +1,114 @@ +{ + "evaluatorTypeId": "uipath-llm-judge-output-semantic-similarity", + "evaluatorConfigSchema": { + "$defs": { + "OutputEvaluationCriteria": { + "description": "Base class for all output evaluation criteria.", + "properties": { + "expected_output": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "string" + } + ], + "title": "Expected Output" + } + }, + "required": [ + "expected_output" + ], + "title": "OutputEvaluationCriteria", + "type": "object" + } + }, + "description": "Configuration for the LLM judge output evaluator.", + "properties": { + "name": { + "default": "LLMJudgeOutputEvaluator", + "title": "Name", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the evaluator", + "title": "Description", + "type": "string" + }, + "default_evaluation_criteria": { + "anyOf": [ + { + "$ref": "#/$defs/OutputEvaluationCriteria" + }, + { + "type": "null" + } + ], + "default": null + }, + "prompt": { + "default": "As an expert evaluator, analyze the semantic similarity of these JSON contents to determine a score from 0-100. Focus on comparing the meaning and contextual equivalence of corresponding fields, accounting for alternative valid expressions, synonyms, and reasonable variations in language while maintaining high standards for accuracy and completeness. Provide your score with a justification, explaining briefly and concisely why you gave that score.\n----\nExpectedOutput:\n{{ExpectedOutput}}\n----\nActualOutput:\n{{ActualOutput}}", + "title": "Prompt", + "type": "string" + }, + "model": { + "default": "", + "title": "Model", + "type": "string" + }, + "temperature": { + "default": 0.0, + "title": "Temperature", + "type": "number" + }, + "max_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Tokens" + }, + "target_output_key": { + "default": "*", + "description": "Key to extract output from agent execution", + "title": "Target Output Key", + "type": "string" + } + }, + "title": "LLMJudgeOutputEvaluatorConfig", + "type": "object" + }, + "evaluationCriteriaSchema": { + "description": "Base class for all output evaluation criteria.", + "properties": { + "expected_output": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "string" + } + ], + "title": "Expected Output" + } + }, + "required": [ + "expected_output" + ], + "title": "OutputEvaluationCriteria", + "type": "object" + }, + "justificationSchema": { + "type": "string" + } +} \ No newline at end of file diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeStrictJSONSimilarityOutputEvaluator.json b/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeStrictJSONSimilarityOutputEvaluator.json new file mode 100644 index 000000000..0fffbbe81 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeStrictJSONSimilarityOutputEvaluator.json @@ -0,0 +1,114 @@ +{ + "evaluatorTypeId": "uipath-llm-judge-output-strict-json-similarity", + "evaluatorConfigSchema": { + "$defs": { + "OutputEvaluationCriteria": { + "description": "Base class for all output evaluation criteria.", + "properties": { + "expected_output": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "string" + } + ], + "title": "Expected Output" + } + }, + "required": [ + "expected_output" + ], + "title": "OutputEvaluationCriteria", + "type": "object" + } + }, + "description": "Configuration for the LLM judge strict JSON similarity output evaluator.", + "properties": { + "name": { + "default": "LLMJudgeStrictJSONSimilarityOutputEvaluator", + "title": "Name", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the evaluator", + "title": "Description", + "type": "string" + }, + "default_evaluation_criteria": { + "anyOf": [ + { + "$ref": "#/$defs/OutputEvaluationCriteria" + }, + { + "type": "null" + } + ], + "default": null + }, + "prompt": { + "default": "ExpectedOutput (ground truth):\n{{ExpectedOutput}}\n\nActualOutput (model answer):\n{{ActualOutput}}", + "title": "Prompt", + "type": "string" + }, + "model": { + "default": "", + "title": "Model", + "type": "string" + }, + "temperature": { + "default": 0.0, + "title": "Temperature", + "type": "number" + }, + "max_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Tokens" + }, + "target_output_key": { + "default": "*", + "description": "Key to extract output from agent execution", + "title": "Target Output Key", + "type": "string" + } + }, + "title": "LLMJudgeStrictJSONSimilarityOutputEvaluatorConfig", + "type": "object" + }, + "evaluationCriteriaSchema": { + "description": "Base class for all output evaluation criteria.", + "properties": { + "expected_output": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "string" + } + ], + "title": "Expected Output" + } + }, + "required": [ + "expected_output" + ], + "title": "OutputEvaluationCriteria", + "type": "object" + }, + "justificationSchema": { + "type": "string" + } +} \ No newline at end of file diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeTrajectoryEvaluator.json b/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeTrajectoryEvaluator.json new file mode 100644 index 000000000..8695fb738 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeTrajectoryEvaluator.json @@ -0,0 +1,92 @@ +{ + "evaluatorTypeId": "uipath-llm-judge-trajectory-similarity", + "evaluatorConfigSchema": { + "$defs": { + "TrajectoryEvaluationCriteria": { + "description": "Evaluation criteria for trajectory-based evaluations.", + "properties": { + "expected_agent_behavior": { + "title": "Expected Agent Behavior", + "type": "string" + } + }, + "required": [ + "expected_agent_behavior" + ], + "title": "TrajectoryEvaluationCriteria", + "type": "object" + } + }, + "description": "Configuration for the llm judge trajectory evaluator.", + "properties": { + "name": { + "default": "LLMJudgeTrajectoryEvaluator", + "title": "Name", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the evaluator", + "title": "Description", + "type": "string" + }, + "default_evaluation_criteria": { + "anyOf": [ + { + "$ref": "#/$defs/TrajectoryEvaluationCriteria" + }, + { + "type": "null" + } + ], + "default": null + }, + "prompt": { + "default": "As an expert evaluator, determine how well the agent performed on a scale of 0-100. Focus on whether the agent's actions and outputs matched the expected behavior, while allowing for alternative valid expressions and reasonable variations in language. Maintain high standards for accuracy and completeness. Provide your score with a brief and clear justification explaining your reasoning.\n----\nAgentInput:\n{{UserOrSyntheticInput}}\n----\nExpectedAgentBehavior:\n{{ExpectedAgentBehavior}}\n----\nAgentRunHistory:\n{{AgentRunHistory}}\n", + "title": "Prompt", + "type": "string" + }, + "model": { + "default": "", + "title": "Model", + "type": "string" + }, + "temperature": { + "default": 0.0, + "title": "Temperature", + "type": "number" + }, + "max_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Tokens" + } + }, + "title": "LLMJudgeTrajectoryEvaluatorConfig", + "type": "object" + }, + "evaluationCriteriaSchema": { + "description": "Evaluation criteria for trajectory-based evaluations.", + "properties": { + "expected_agent_behavior": { + "title": "Expected Agent Behavior", + "type": "string" + } + }, + "required": [ + "expected_agent_behavior" + ], + "title": "TrajectoryEvaluationCriteria", + "type": "object" + }, + "justificationSchema": { + "type": "string" + } +} \ No newline at end of file diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeTrajectorySimulationEvaluator.json b/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeTrajectorySimulationEvaluator.json new file mode 100644 index 000000000..006e24202 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/LLMJudgeTrajectorySimulationEvaluator.json @@ -0,0 +1,92 @@ +{ + "evaluatorTypeId": "uipath-llm-judge-trajectory-simulation", + "evaluatorConfigSchema": { + "$defs": { + "TrajectoryEvaluationCriteria": { + "description": "Evaluation criteria for trajectory-based evaluations.", + "properties": { + "expected_agent_behavior": { + "title": "Expected Agent Behavior", + "type": "string" + } + }, + "required": [ + "expected_agent_behavior" + ], + "title": "TrajectoryEvaluationCriteria", + "type": "object" + } + }, + "description": "Configuration for the llm judge simulation trajectory evaluator.", + "properties": { + "name": { + "default": "LLMJudgeTrajectorySimulationEvaluator", + "title": "Name", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the evaluator", + "title": "Description", + "type": "string" + }, + "default_evaluation_criteria": { + "anyOf": [ + { + "$ref": "#/$defs/TrajectoryEvaluationCriteria" + }, + { + "type": "null" + } + ], + "default": null + }, + "prompt": { + "default": "As an expert evaluator, determine how well the agent did on a scale of 0-100. Focus on if the simulation was successful and if the agent behaved according to the expected output accounting for alternative valid expressions, and reasonable variations in language while maintaining high standards for accuracy and completeness. Provide your score with a justification, explaining briefly and concisely why you gave that score.\n----\nAgentInput:\n{{UserOrSyntheticInput}}\n----\nSimulationInstructions:\n{{SimulationInstructions}}\n----\nExpectedAgentBehavior:\n{{ExpectedAgentBehavior}}\n----\nAgentRunHistory:\n{{AgentRunHistory}}\n", + "title": "Prompt", + "type": "string" + }, + "model": { + "default": "", + "title": "Model", + "type": "string" + }, + "temperature": { + "default": 0.0, + "title": "Temperature", + "type": "number" + }, + "max_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Max Tokens" + } + }, + "title": "LLMJudgeTrajectorySimulationEvaluatorConfig", + "type": "object" + }, + "evaluationCriteriaSchema": { + "description": "Evaluation criteria for trajectory-based evaluations.", + "properties": { + "expected_agent_behavior": { + "title": "Expected Agent Behavior", + "type": "string" + } + }, + "required": [ + "expected_agent_behavior" + ], + "title": "TrajectoryEvaluationCriteria", + "type": "object" + }, + "justificationSchema": { + "type": "string" + } +} \ No newline at end of file diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallArgsEvaluator.json b/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallArgsEvaluator.json new file mode 100644 index 000000000..645ada479 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallArgsEvaluator.json @@ -0,0 +1,137 @@ +{ + "evaluatorTypeId": "uipath-tool-call-args", + "evaluatorConfigSchema": { + "$defs": { + "ToolCall": { + "description": "Represents a tool call with its arguments.", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "args": { + "additionalProperties": true, + "title": "Args", + "type": "object" + } + }, + "required": [ + "name", + "args" + ], + "title": "ToolCall", + "type": "object" + }, + "ToolCallArgsEvaluationCriteria": { + "description": "Evaluation criteria for the tool call order evaluator.", + "properties": { + "tool_calls": { + "items": { + "$ref": "#/$defs/ToolCall" + }, + "title": "Tool Calls", + "type": "array" + } + }, + "required": [ + "tool_calls" + ], + "title": "ToolCallArgsEvaluationCriteria", + "type": "object" + } + }, + "description": "Configuration for the tool call count evaluator.", + "properties": { + "name": { + "default": "ToolCallArgsEvaluator", + "title": "Name", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the evaluator", + "title": "Description", + "type": "string" + }, + "default_evaluation_criteria": { + "anyOf": [ + { + "$ref": "#/$defs/ToolCallArgsEvaluationCriteria" + }, + { + "type": "null" + } + ], + "default": null + }, + "strict": { + "default": false, + "title": "Strict", + "type": "boolean" + }, + "subset": { + "default": false, + "title": "Subset", + "type": "boolean" + } + }, + "title": "ToolCallArgsEvaluatorConfig", + "type": "object" + }, + "evaluationCriteriaSchema": { + "$defs": { + "ToolCall": { + "description": "Represents a tool call with its arguments.", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "args": { + "additionalProperties": true, + "title": "Args", + "type": "object" + } + }, + "required": [ + "name", + "args" + ], + "title": "ToolCall", + "type": "object" + } + }, + "description": "Evaluation criteria for the tool call order evaluator.", + "properties": { + "tool_calls": { + "items": { + "$ref": "#/$defs/ToolCall" + }, + "title": "Tool Calls", + "type": "array" + } + }, + "required": [ + "tool_calls" + ], + "title": "ToolCallArgsEvaluationCriteria", + "type": "object" + }, + "justificationSchema": { + "description": "Justification for the tool call args evaluator.", + "properties": { + "explained_tool_calls_args": { + "additionalProperties": { + "type": "string" + }, + "title": "Explained Tool Calls Args", + "type": "object" + } + }, + "required": [ + "explained_tool_calls_args" + ], + "title": "ToolCallArgsEvaluatorJustification", + "type": "object" + } +} \ No newline at end of file diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallCountEvaluator.json b/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallCountEvaluator.json new file mode 100644 index 000000000..56b56d543 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallCountEvaluator.json @@ -0,0 +1,110 @@ +{ + "evaluatorTypeId": "uipath-tool-call-count", + "evaluatorConfigSchema": { + "$defs": { + "ToolCallCountEvaluationCriteria": { + "description": "Evaluation criteria for the tool call count evaluator.", + "properties": { + "tool_calls_count": { + "additionalProperties": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "type": "array" + }, + "title": "Tool Calls Count", + "type": "object" + } + }, + "required": [ + "tool_calls_count" + ], + "title": "ToolCallCountEvaluationCriteria", + "type": "object" + } + }, + "description": "Configuration for the tool call count evaluator.", + "properties": { + "name": { + "default": "ToolCallCountEvaluator", + "title": "Name", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the evaluator", + "title": "Description", + "type": "string" + }, + "default_evaluation_criteria": { + "anyOf": [ + { + "$ref": "#/$defs/ToolCallCountEvaluationCriteria" + }, + { + "type": "null" + } + ], + "default": null + }, + "strict": { + "default": false, + "title": "Strict", + "type": "boolean" + } + }, + "title": "ToolCallCountEvaluatorConfig", + "type": "object" + }, + "evaluationCriteriaSchema": { + "description": "Evaluation criteria for the tool call count evaluator.", + "properties": { + "tool_calls_count": { + "additionalProperties": { + "maxItems": 2, + "minItems": 2, + "prefixItems": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "type": "array" + }, + "title": "Tool Calls Count", + "type": "object" + } + }, + "required": [ + "tool_calls_count" + ], + "title": "ToolCallCountEvaluationCriteria", + "type": "object" + }, + "justificationSchema": { + "description": "Justification for the tool call count evaluator.", + "properties": { + "explained_tool_calls_count": { + "additionalProperties": { + "type": "string" + }, + "title": "Explained Tool Calls Count", + "type": "object" + } + }, + "required": [ + "explained_tool_calls_count" + ], + "title": "ToolCallCountEvaluatorJustification", + "type": "object" + } +} \ No newline at end of file diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallOrderEvaluator.json b/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallOrderEvaluator.json new file mode 100644 index 000000000..568890eb1 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallOrderEvaluator.json @@ -0,0 +1,106 @@ +{ + "evaluatorTypeId": "uipath-tool-call-order", + "evaluatorConfigSchema": { + "$defs": { + "ToolCallOrderEvaluationCriteria": { + "description": "Evaluation criteria for the tool call order evaluator.", + "properties": { + "tool_calls_order": { + "items": { + "type": "string" + }, + "title": "Tool Calls Order", + "type": "array" + } + }, + "required": [ + "tool_calls_order" + ], + "title": "ToolCallOrderEvaluationCriteria", + "type": "object" + } + }, + "description": "Configuration for the tool call count evaluator.", + "properties": { + "name": { + "default": "ToolCallOrderEvaluator", + "title": "Name", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the evaluator", + "title": "Description", + "type": "string" + }, + "default_evaluation_criteria": { + "anyOf": [ + { + "$ref": "#/$defs/ToolCallOrderEvaluationCriteria" + }, + { + "type": "null" + } + ], + "default": null + }, + "strict": { + "default": false, + "title": "Strict", + "type": "boolean" + } + }, + "title": "ToolCallOrderEvaluatorConfig", + "type": "object" + }, + "evaluationCriteriaSchema": { + "description": "Evaluation criteria for the tool call order evaluator.", + "properties": { + "tool_calls_order": { + "items": { + "type": "string" + }, + "title": "Tool Calls Order", + "type": "array" + } + }, + "required": [ + "tool_calls_order" + ], + "title": "ToolCallOrderEvaluationCriteria", + "type": "object" + }, + "justificationSchema": { + "description": "Justification for the tool call order evaluator.", + "properties": { + "actual_tool_calls_order": { + "items": { + "type": "string" + }, + "title": "Actual Tool Calls Order", + "type": "array" + }, + "expected_tool_calls_order": { + "items": { + "type": "string" + }, + "title": "Expected Tool Calls Order", + "type": "array" + }, + "lcs": { + "items": { + "type": "string" + }, + "title": "Lcs", + "type": "array" + } + }, + "required": [ + "actual_tool_calls_order", + "expected_tool_calls_order", + "lcs" + ], + "title": "ToolCallOrderEvaluatorJustification", + "type": "object" + } +} \ No newline at end of file diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallOutputEvaluator.json b/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallOutputEvaluator.json new file mode 100644 index 000000000..73455592a --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/ToolCallOutputEvaluator.json @@ -0,0 +1,130 @@ +{ + "evaluatorTypeId": "uipath-tool-call-output", + "evaluatorConfigSchema": { + "$defs": { + "ToolCallOutputEvaluationCriteria": { + "description": "Evaluation criteria for the tool call order evaluator.", + "properties": { + "tool_outputs": { + "items": { + "$ref": "#/$defs/ToolOutput" + }, + "title": "Tool Outputs", + "type": "array" + } + }, + "required": [ + "tool_outputs" + ], + "title": "ToolCallOutputEvaluationCriteria", + "type": "object" + }, + "ToolOutput": { + "description": "Represents a tool output with its output.", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "output": { + "title": "Output", + "type": "string" + } + }, + "required": [ + "name", + "output" + ], + "title": "ToolOutput", + "type": "object" + } + }, + "description": "Configuration for the tool call count evaluator.", + "properties": { + "name": { + "default": "ToolCallOutputEvaluator", + "title": "Name", + "type": "string" + }, + "description": { + "default": "", + "description": "The description of the evaluator", + "title": "Description", + "type": "string" + }, + "default_evaluation_criteria": { + "anyOf": [ + { + "$ref": "#/$defs/ToolCallOutputEvaluationCriteria" + }, + { + "type": "null" + } + ], + "default": null + }, + "strict": { + "default": false, + "title": "Strict", + "type": "boolean" + } + }, + "title": "ToolCallOutputEvaluatorConfig", + "type": "object" + }, + "evaluationCriteriaSchema": { + "$defs": { + "ToolOutput": { + "description": "Represents a tool output with its output.", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "output": { + "title": "Output", + "type": "string" + } + }, + "required": [ + "name", + "output" + ], + "title": "ToolOutput", + "type": "object" + } + }, + "description": "Evaluation criteria for the tool call order evaluator.", + "properties": { + "tool_outputs": { + "items": { + "$ref": "#/$defs/ToolOutput" + }, + "title": "Tool Outputs", + "type": "array" + } + }, + "required": [ + "tool_outputs" + ], + "title": "ToolCallOutputEvaluationCriteria", + "type": "object" + }, + "justificationSchema": { + "description": "Justification for the tool call output evaluator.", + "properties": { + "explained_tool_calls_outputs": { + "additionalProperties": { + "type": "string" + }, + "title": "Explained Tool Calls Outputs", + "type": "object" + } + }, + "required": [ + "explained_tool_calls_outputs" + ], + "title": "ToolCallOutputEvaluatorJustification", + "type": "object" + } +} \ No newline at end of file diff --git a/packages/uipath-eval/src/uipath_eval/evaluators_types/generate_types.py b/packages/uipath-eval/src/uipath_eval/evaluators_types/generate_types.py new file mode 100644 index 000000000..22fb38d65 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/evaluators_types/generate_types.py @@ -0,0 +1,31 @@ +"""Generate the JSON types for all evaluators.""" + +import json +import os +from typing import Any + +from ..evaluators import EVALUATORS + + +def generate_evaluator_json_types( + write_to_file: bool = False, indent: int | str | None = None +) -> dict[str, Any]: + """Generate the JSON types for all evaluators.""" + OUTPUT_PATH = os.path.dirname(os.path.abspath(__file__)) + + os.makedirs(OUTPUT_PATH, exist_ok=True) + + evaluator_json_types = {} + for evaluator in EVALUATORS: + evaluator_json_type = evaluator.generate_json_type() + evaluator_json_types[evaluator.__name__] = evaluator_json_type + if write_to_file: + with open( + os.path.join(OUTPUT_PATH, f"{evaluator.__name__}.json"), "w" + ) as f: + json.dump(evaluator_json_type, f, indent=indent) + return evaluator_json_types + + +if __name__ == "__main__": + generate_evaluator_json_types(write_to_file=True, indent=2) diff --git a/packages/uipath-eval/src/uipath_eval/mocks/__init__.py b/packages/uipath-eval/src/uipath_eval/mocks/__init__.py new file mode 100644 index 000000000..18460be41 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/mocks/__init__.py @@ -0,0 +1,37 @@ +"""Mocking types for evaluation and simulation.""" + +from ._types import ( + ExampleCall, + InputMockingStrategy, + KnownMockingStrategy, + LLMMockingStrategy, + MockingAnswer, + MockingAnswerType, + MockingArgument, + MockingBehavior, + MockingContext, + MockingStrategy, + MockingStrategyType, + MockitoMockingStrategy, + ModelSettings, + ToolSimulation, + UnknownMockingStrategy, +) + +__all__ = [ + "ExampleCall", + "InputMockingStrategy", + "KnownMockingStrategy", + "LLMMockingStrategy", + "MockingAnswer", + "MockingAnswerType", + "MockingArgument", + "MockingBehavior", + "MockingContext", + "MockingStrategy", + "MockingStrategyType", + "MockitoMockingStrategy", + "ModelSettings", + "ToolSimulation", + "UnknownMockingStrategy", +] diff --git a/packages/uipath-eval/src/uipath_eval/mocks/_types.py b/packages/uipath-eval/src/uipath_eval/mocks/_types.py new file mode 100644 index 000000000..827569879 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/mocks/_types.py @@ -0,0 +1,141 @@ +"""Mocking types for evaluation and simulation.""" + +from enum import Enum +from typing import Annotated, Any, Literal, Union + +from pydantic import BaseModel, ConfigDict, Field + + +class MockingStrategyType(str, Enum): + """Supported mocking strategy types.""" + + LLM = "llm" + MOCKITO = "mockito" + UNKNOWN = "unknown" + + +class BaseMockingStrategy(BaseModel): + """Base class for mocking strategies.""" + + pass + + +class ToolSimulation(BaseModel): + """A tool to be simulated during evaluation.""" + + name: str = Field(..., alias="name") + + +class ModelSettings(BaseModel): + """Model generation parameters for LLM-based mocking.""" + + model: str = Field(..., alias="model") + temperature: float | str | None = Field(default=None, alias="temperature") + top_p: float | None = Field(default=None, alias="topP") + top_k: int | None = Field(default=None, alias="topK") + frequency_penalty: float | None = Field(default=None, alias="frequencyPenalty") + presence_penalty: float | None = Field(default=None, alias="presencePenalty") + max_tokens: int | None = Field(default=None, alias="maxTokens") + + +class LLMMockingStrategy(BaseMockingStrategy): + """Mocking strategy that uses an LLM to generate simulated tool responses.""" + + type: Literal[MockingStrategyType.LLM] = MockingStrategyType.LLM + prompt: str = Field(..., alias="prompt") + tools_to_simulate: list[ToolSimulation] = Field(..., alias="toolsToSimulate") + model: ModelSettings | None = Field(None, alias="model") + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + +class InputMockingStrategy(BaseModel): + """Strategy for generating mocked inputs via LLM.""" + + prompt: str = Field(..., alias="prompt") + model: ModelSettings | None = Field(None, alias="model") + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + +class MockingArgument(BaseModel): + """Arguments matcher for mockito-style mocking.""" + + args: list[Any] = Field(default_factory=lambda: [], alias="args") + kwargs: dict[str, Any] = Field(default_factory=lambda: {}, alias="kwargs") + + +class MockingAnswerType(str, Enum): + """Type of answer a mock should produce.""" + + RETURN = "return" + RAISE = "raise" + + +class MockingAnswer(BaseModel): + """A mock answer definition (return value or exception).""" + + type: MockingAnswerType + value: Any = Field(..., alias="value") + + +class MockingBehavior(BaseModel): + """Defines how a mocked function should behave.""" + + function: str = Field(..., alias="function") + arguments: MockingArgument | None = Field(default=None, alias="arguments") + then: list[MockingAnswer] = Field(..., alias="then") + + +class MockitoMockingStrategy(BaseMockingStrategy): + """Mocking strategy using mockito-style behavior definitions.""" + + type: Literal[MockingStrategyType.MOCKITO] = MockingStrategyType.MOCKITO + behaviors: list[MockingBehavior] = Field(..., alias="config") + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + +KnownMockingStrategy = Annotated[ + Union[LLMMockingStrategy, MockitoMockingStrategy], + Field(discriminator="type"), +] + + +class UnknownMockingStrategy(BaseMockingStrategy): + """Fallback for unrecognized mocking strategy types.""" + + type: str = Field(..., alias="type") + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) + + +MockingStrategy = Union[KnownMockingStrategy, UnknownMockingStrategy] + + +class MockingContext(BaseModel): + """Execution context for mocking, holding strategy and inputs.""" + + strategy: MockingStrategy | None + inputs: dict[str, Any] = Field(default_factory=lambda: {}) + name: str = Field(default="debug") + + +class ExampleCall(BaseModel): + """Example call for a resource containing resource I/O.""" + + id: str = Field(..., alias="id") + input: str = Field(..., alias="input") + output: str = Field(..., alias="output") + + model_config = ConfigDict( + validate_by_name=True, validate_by_alias=True, extra="allow" + ) diff --git a/packages/uipath-eval/src/uipath_eval/models/__init__.py b/packages/uipath-eval/src/uipath_eval/models/__init__.py new file mode 100644 index 000000000..590d9e291 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/models/__init__.py @@ -0,0 +1,35 @@ +"""UiPath evaluation data models.""" + +from uipath_eval.models.models import ( + AgentExecution, + BooleanEvaluationResult, + ErrorEvaluationResult, + EvalItemResult, + EvaluationResult, + EvaluationResultDto, + EvaluatorType, + LegacyEvaluatorCategory, + LegacyEvaluatorType, + LLMResponse, + NumericEvaluationResult, + ScoreType, + ToolCall, + ToolOutput, +) + +__all__ = [ + "AgentExecution", + "EvaluationResult", + "EvaluationResultDto", + "LLMResponse", + "LegacyEvaluatorCategory", + "LegacyEvaluatorType", + "EvaluatorType", + "ScoreType", + "EvalItemResult", + "BooleanEvaluationResult", + "NumericEvaluationResult", + "ErrorEvaluationResult", + "ToolCall", + "ToolOutput", +] diff --git a/packages/uipath-eval/src/uipath_eval/models/_conversational_utils.py b/packages/uipath-eval/src/uipath_eval/models/_conversational_utils.py new file mode 100644 index 000000000..d2ad9f11c --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/models/_conversational_utils.py @@ -0,0 +1,307 @@ +import uuid +from datetime import datetime, timezone +from typing import Any, List, Literal + +from pydantic import BaseModel, Field +from uipath.core.chat import ( + UiPathConversationContentPart, + UiPathConversationContentPartData, + UiPathConversationMessage, + UiPathConversationMessageData, + UiPathConversationToolCall, + UiPathConversationToolCallData, + UiPathConversationToolCallResult, + UiPathExternalValue, + UiPathInlineValue, +) + +# Types for legacy conversational-agent evaluation input/outputs. + + +class LegacyConversationalEvalJobAttachmentReference(BaseModel): + """File attachment reference in eval messages.""" + + id: str = Field(..., alias="ID") + full_name: str = Field(..., alias="FullName") + mime_type: str = Field(..., alias="MimeType") + + +class LegacyConversationalEvalOutputToolCall(BaseModel): + """Tool call in eval output schema (no result field).""" + + name: str + arguments: dict[str, Any] + + +class LegacyConversationalEvalInputToolCallResult(BaseModel): + """Tool call result in eval input schema.""" + + value: Any + is_error: bool | None = Field(default=None, alias="isError") + + +class LegacyConversationalEvalInputToolCall(LegacyConversationalEvalOutputToolCall): + """Tool call in eval input schema (extends output tool call with result).""" + + result: LegacyConversationalEvalInputToolCallResult + + +class LegacyConversationalEvalMessage(BaseModel): + """Base eval message type.""" + + role: Literal["agent", "user"] + text: str + + +class LegacyConversationalEvalUserMessage(LegacyConversationalEvalMessage): + """User message in eval schema.""" + + role: Literal["user"] = "user" + attachments: list[LegacyConversationalEvalJobAttachmentReference] | None = Field( + default=None + ) + + +class LegacyConversationalEvalInputAgentMessage(LegacyConversationalEvalMessage): + """Agent message in eval input schema (input tool-calls contain results field).""" + + role: Literal["agent"] = "agent" + tool_calls: list[LegacyConversationalEvalInputToolCall] | None = Field( + default=None, alias="toolCalls" + ) + + +class LegacyConversationalEvalOutputAgentMessage(LegacyConversationalEvalMessage): + """Agent message in eval output schema (output tool-calls don't contain result field).""" + + role: Literal["agent"] = "agent" + tool_calls: list[LegacyConversationalEvalOutputToolCall] | None = Field( + default=None, alias="toolCalls" + ) + + +class LegacyConversationalEvalInput(BaseModel): + """Complete conversational eval input schema. + + conversationHistory: Array of exchanges, where each exchange is + [userMessage, ...agentMessages[]] + currentUserPrompt: The current user message to evaluate + """ + + conversation_history: list[ + list[ + LegacyConversationalEvalUserMessage + | LegacyConversationalEvalInputAgentMessage + ] + ] = Field(alias="conversationHistory") + current_user_prompt: LegacyConversationalEvalUserMessage = Field( + alias="currentUserPrompt" + ) + + +class LegacyConversationalEvalOutput(BaseModel): + """Complete eval output schema matching TypeScript definition. + + agentResponse: Sequence of agent messages ending with a message without tool calls + """ + + agent_response: list[LegacyConversationalEvalOutputAgentMessage] = Field( + alias="agentResponse" + ) + + +# Mapper functions to convert between UiPath standard Message format and legacy conversational formats + + +class UiPathLegacyEvalChatMessagesMapper: + @staticmethod + def legacy_conversational_eval_input_to_uipath_message_list( + eval_input: LegacyConversationalEvalInput, + ) -> List[UiPathConversationMessage]: + """Convert legacy eval input format to list of UiPathConversationMessage.""" + messages: List[UiPathConversationMessage] = [] + timestamp = ( + datetime.now(timezone.utc) + .isoformat(timespec="milliseconds") + .replace("+00:00", "Z") + ) + + # Process conversation history (list of exchanges) + for eval_exchange in eval_input.conversation_history: + for eval_message in eval_exchange: + if eval_message.role == "user": + # Convert user message + content_parts = ( + [ + UiPathConversationContentPart( + content_part_id=str(uuid.uuid4()), + mime_type="text/plain", + data=UiPathInlineValue(inline=eval_message.text), + citations=[], + created_at=timestamp, + updated_at=timestamp, + ) + ] + if eval_message.text + else [] + ) + + if eval_message.attachments: + for attachment in eval_message.attachments: + content_parts.append( + UiPathConversationContentPart( + content_part_id=str(uuid.uuid4()), + mime_type=attachment.mime_type, + data=UiPathExternalValue( + uri=f"urn:uipath:cas:file:orchestrator:{attachment.id}" + ), + name=attachment.full_name, + citations=[], + created_at=timestamp, + updated_at=timestamp, + ) + ) + messages.append( + UiPathConversationMessage( + message_id=str(uuid.uuid4()), + role="user", + content_parts=content_parts, + tool_calls=[], + interrupts=[], + created_at=timestamp, + updated_at=timestamp, + ) + ) + elif eval_message.role == "agent": + # Convert agent message + content_parts = ( + [ + UiPathConversationContentPart( + content_part_id=str(uuid.uuid4()), + mime_type="text/markdown", + data=UiPathInlineValue(inline=eval_message.text), + citations=[], + created_at=timestamp, + updated_at=timestamp, + ) + ] + if eval_message.text + else [] + ) + + # Convert tool calls if present + tool_calls: List[UiPathConversationToolCall] = [] + if eval_message.tool_calls: + for tc in eval_message.tool_calls: + tool_call = UiPathConversationToolCall( + tool_call_id=str(uuid.uuid4()), + name=tc.name, + input=tc.arguments, + timestamp=timestamp, + result=UiPathConversationToolCallResult( + timestamp=timestamp, + output=tc.result.value, + is_error=tc.result.is_error, + ), + created_at=timestamp, + updated_at=timestamp, + ) + tool_calls.append(tool_call) + + messages.append( + UiPathConversationMessage( + message_id=str(uuid.uuid4()), + role="assistant", + content_parts=content_parts, + tool_calls=tool_calls, + interrupts=[], + created_at=timestamp, + updated_at=timestamp, + ) + ) + + # Add current user prompt + content_parts = ( + [ + UiPathConversationContentPart( + content_part_id=str(uuid.uuid4()), + mime_type="text/plain", + data=UiPathInlineValue(inline=eval_input.current_user_prompt.text), + citations=[], + created_at=timestamp, + updated_at=timestamp, + ) + ] + if eval_input.current_user_prompt.text + else [] + ) + + if eval_input.current_user_prompt.attachments: + for attachment in eval_input.current_user_prompt.attachments: + content_parts.append( + UiPathConversationContentPart( + content_part_id=str(uuid.uuid4()), + mime_type=attachment.mime_type, + data=UiPathExternalValue( + uri=f"urn:uipath:cas:file:orchestrator:{attachment.id}" + ), + name=attachment.full_name, + citations=[], + created_at=timestamp, + updated_at=timestamp, + ) + ) + + messages.append( + UiPathConversationMessage( + message_id=str(uuid.uuid4()), + role="user", + content_parts=content_parts, + tool_calls=[], + interrupts=[], + created_at=timestamp, + updated_at=timestamp, + ) + ) + + return messages + + @staticmethod + def legacy_conversational_eval_output_to_uipath_message_data_list( + eval_output: LegacyConversationalEvalOutput, + ) -> List[UiPathConversationMessageData]: + """Convert legacy eval output format to list of UiPathConversationMessageData.""" + messages: List[UiPathConversationMessageData] = [] + + for eval_agent_message in eval_output.agent_response: + content_parts = ( + [ + UiPathConversationContentPartData( + mime_type="text/markdown", + data=UiPathInlineValue(inline=eval_agent_message.text), + citations=[], + ) + ] + if eval_agent_message.text + else [] + ) + + tool_calls: List[UiPathConversationToolCallData] = [] + if eval_agent_message.tool_calls: + for tc in eval_agent_message.tool_calls: + tool_call = UiPathConversationToolCallData( + name=tc.name, + input=tc.arguments, + ) + tool_calls.append(tool_call) + + messages.append( + UiPathConversationMessageData( + role="assistant", + content_parts=content_parts, + tool_calls=tool_calls, + interrupts=[], + ) + ) + + return messages diff --git a/packages/uipath-eval/src/uipath_eval/models/evaluation_set.py b/packages/uipath-eval/src/uipath_eval/models/evaluation_set.py new file mode 100644 index 000000000..45af3c61a --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/models/evaluation_set.py @@ -0,0 +1,197 @@ +"""Evaluation set models.""" + +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel + +from ..mocks._types import ( + InputMockingStrategy, + MockingStrategy, + ToolSimulation, +) +from ._conversational_utils import ( + LegacyConversationalEvalInput, + LegacyConversationalEvalOutput, +) + + +class EvaluatorReference(BaseModel): + """Reference to an evaluator with optional weight. + + Can be constructed from: + - A string (evaluator ID): EvaluatorReference(ref="evaluator-id") + - A dict with ref and optional weight: EvaluatorReference(ref="evaluator-id", weight=2.0) + """ + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + ref: str = Field(..., description="Path to the evaluator configuration file") + weight: float = Field( + default=1.0, + description="Weight for this evaluator in scoring calculations", + ge=0, + ) + + @classmethod + def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> Any: + """Allow creating EvaluatorReference from a string or dict.""" + from pydantic_core import core_schema + + def validate_from_str(value: str) -> dict[str, Any]: + """Convert a string to a dict with ref field.""" + return {"ref": value} + + def serialize(instance: "EvaluatorReference") -> Any: + if instance.weight != 1.0: + return {"ref": instance.ref, "weight": instance.weight} + return instance.ref + + python_schema = handler(source_type) + return core_schema.union_schema( + [ + core_schema.chain_schema( + [ + core_schema.str_schema(), + core_schema.no_info_plain_validator_function(validate_from_str), + python_schema, + ] + ), + python_schema, + ], + serialization=core_schema.plain_serializer_function_ser_schema(serialize), + ) + + +class EvaluationSetModelSettings(BaseModel): + """Model setting overrides within evaluation sets with ID.""" + + model_config = ConfigDict(populate_by_name=True) + + id: str = Field(..., alias="id") + model_name: str = Field(..., alias="modelName") + temperature: float | str | None = Field(default=None, alias="temperature") + + +class EvaluationItem(BaseModel): + """Individual evaluation item within an evaluation set.""" + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + id: str + name: str + inputs: dict[str, Any] + expected_output: dict[str, Any] | str | None = Field( + default=None, alias="expectedOutput" + ) + evaluation_criterias: dict[str, dict[str, Any] | None] = Field( + ..., alias="evaluationCriterias" + ) + expected_agent_behavior: str = Field(default="", alias="expectedAgentBehavior") + mocking_strategy: MockingStrategy | None = Field( + default=None, + alias="mockingStrategy", + ) + input_mocking_strategy: InputMockingStrategy | None = Field( + default=None, + alias="inputMockingStrategy", + ) + + +class LegacyEvaluationItem(BaseModel): + """Individual evaluation item within an evaluation set.""" + + model_config = ConfigDict( + alias_generator=to_camel, populate_by_name=True, extra="allow" + ) + + id: str + name: str + inputs: dict[str, Any] + expected_output: dict[str, Any] + expected_agent_behavior: str = Field(default="", alias="expectedAgentBehavior") + eval_set_id: str = Field(alias="evalSetId") + created_at: str = Field(alias="createdAt") + updated_at: str = Field(alias="updatedAt") + simulate_input: bool | None = Field(default=None, alias="simulateInput") + input_generation_instructions: str | None = Field( + default=None, alias="inputGenerationInstructions" + ) + simulate_tools: bool | None = Field(default=None, alias="simulateTools") + simulation_instructions: str | None = Field( + default=None, alias="simulationInstructions" + ) + tools_to_simulate: list[ToolSimulation] = Field( + default_factory=list, alias="toolsToSimulate" + ) + conversational_inputs: LegacyConversationalEvalInput | None = Field( + default=None, alias="conversationalInputs" + ) + conversational_expected_output: LegacyConversationalEvalOutput | None = Field( + default=None, alias="conversationalExpectedOutput" + ) + + +class EvaluationSet(BaseModel): + """Complete evaluation set model.""" + + model_config = ConfigDict( + alias_generator=to_camel, populate_by_name=True, extra="allow" + ) + + id: str + name: str + version: Literal["1.0"] = "1.0" + evaluator_refs: list[str] = Field(default_factory=list) + evaluator_configs: list[EvaluatorReference] = Field( + default_factory=list, alias="evaluatorConfigs" + ) + evaluations: list[EvaluationItem] = Field(default_factory=list) + model_settings: list[EvaluationSetModelSettings] = Field( + default_factory=list, alias="modelSettings" + ) + + def extract_selected_evals(self, eval_ids: list[str]) -> None: + """Filter evaluations to only include those with specified IDs.""" + selected_evals: list[EvaluationItem] = [] + remaining_ids = set(eval_ids) + for evaluation in self.evaluations: + if evaluation.id in remaining_ids: + selected_evals.append(evaluation) + remaining_ids.remove(evaluation.id) + if len(remaining_ids) > 0: + raise ValueError("Unknown evaluation ids: {}".format(remaining_ids)) + self.evaluations = selected_evals + + +class LegacyEvaluationSet(BaseModel): + """Complete evaluation set model.""" + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + id: str + file_name: str = Field(..., alias="fileName") + evaluator_refs: list[str] = Field(default_factory=list) + evaluator_configs: list[EvaluatorReference] = Field( + default_factory=list, alias="evaluatorConfigs" + ) + evaluations: list[LegacyEvaluationItem] = Field(default_factory=list) + name: str + batch_size: int = Field(10, alias="batchSize") + timeout_minutes: int = Field(default=20, alias="timeoutMinutes") + model_settings: list[EvaluationSetModelSettings] = Field( + default_factory=list, alias="modelSettings" + ) + created_at: str = Field(alias="createdAt") + updated_at: str = Field(alias="updatedAt") + + def extract_selected_evals(self, eval_ids: list[str]) -> None: + """Filter evaluations to only include those with specified IDs.""" + selected_evals: list[LegacyEvaluationItem] = [] + remaining_ids = set(eval_ids) + for evaluation in self.evaluations: + if evaluation.id in remaining_ids: + selected_evals.append(evaluation) + remaining_ids.remove(evaluation.id) + if len(remaining_ids) > 0: + raise ValueError("Unknown evaluation ids: {}".format(remaining_ids)) + self.evaluations = selected_evals diff --git a/packages/uipath-eval/src/uipath_eval/models/llm_judge_types.py b/packages/uipath-eval/src/uipath_eval/models/llm_judge_types.py new file mode 100644 index 000000000..9f488bce7 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/models/llm_judge_types.py @@ -0,0 +1,196 @@ +"""Types for LLM judge evaluators.""" + +from enum import Enum + +from pydantic import BaseModel, Field + + +class LLMJudgeOutputSchema(BaseModel): + """Schema for LLM judge output.""" + + justification: str = Field( + ..., + description="A clear analysis of the semantic similarity of the input contents that appears BEFORE reaching a numeric score. It must justify every penalty or lenience, and mention the effects of any deviation.", + ) + score: float = Field( + ..., + description="The final rounded integer between 0 and 100, computed strictly from the rubric in the prompt. It must follow the reasoning and contain only the number-no additional text.", + ) + + +class LLMJudgeStrictJSONSimilarityOutputSchema(BaseModel): + """Schema for LLM judge strict JSON similarity output.""" + + justification: str = Field( + ..., + description="A clear, ≤250-word analysis that appears BEFORE the numeric score. It must discuss every key from ExpectedOutput, state whether each value in ActualOutput is equivalent, partially correct, or incorrect/missing, justify every penalty or lenience, and mention effects of extra keys.", + ) + score: float = Field( + ..., + description="The final rounded integer between 0 and 100, computed strictly from the rubric in the prompt. It must follow the reasoning and contain only the number—no additional text.", + ) + + +class LLMJudgeTrajectoryOutputSchema(BaseModel): + """Schema for LLM judge trajectory output.""" + + justification: str = Field( + ..., + description="A clear analysis of the similarity between the expected behavior and the actual behavior of the agent that appears BEFORE reaching a numeric score. It must justify every penalty or lenience, and mention the effects of any deviation. Include the expected behavior, and the actual behavior of the agent.", + ) + score: float = Field( + ..., + description="The final rounded integer between 0 and 100, computed strictly from the rubric in the prompt. It must follow the reasoning and contain only the number—no additional text.", + ) + + +class LLMJudgePromptTemplates(str, Enum): + """Templates for LLM judge prompts.""" + + LLM_JUDGE_SYSTEM_PROMPT = """You are an expert evaluator tasked with assessing text based on specific criteria. You will be given: +1. An evaluation criterion or question. +2. A text to evaluate. +Your task is to carefully analyze the given text according to the specified criterion. +If the criterion asks for a degree or extent, respond with a numerical score from 0 to 100: +0 means the text does not meet the criterion at all. +100 means the text fully meets the criterion. +If the criterion is a yes/no question or can be answered with true/false, respond with a boolean: true or false. +To submit your evaluation, use the correct tool for the score type. +Never answer using text. Only use the tool to submit your score. +""" + + LLM_JUDGE_DEFAULT_USER_PROMPT = """As an expert evaluator, analyze the semantic similarity of these JSON contents to determine a score from 0-100. Focus on comparing the meaning and contextual equivalence of corresponding fields, accounting for alternative valid expressions, synonyms, and reasonable variations in language while maintaining high standards for accuracy and completeness. Provide your score with a justification, explaining briefly and concisely why you gave that score. +---- +ExpectedOutput: +{{ExpectedOutput}} +---- +ActualOutput: +{{ActualOutput}}""" + + LLM_JUDGE_STRICT_JSON_SIMILARITY_SYSTEM_PROMPT = """You are an impartial grading agent. + +⚠️ STEP 1: MANDATORY KEY INVENTORY (EXACT COUNTING) +List the exact top-level keys by copying them character-for-character: + +Expected keys: ['key1', 'key2', 'key3', ...] +Actual keys: ['key1', 'key2', ...] +N (total expected keys): [exact integer] + +⚠️ STEP 2: DETERMINISTIC KEY MATCHING +For each expected key, check if EXACTLY THE SAME key name exists in actual output: + +Expected Key 'KeyName1': EXISTS in actual? [YES/NO] +Expected Key 'KeyName2': EXISTS in actual? [YES/NO] +[Continue for all expected keys] + +⚠️ STEP 3: EXTRA KEY IDENTIFICATION +List any actual keys not in expected: +Extra keys: ['extrakey1', 'extrakey2', ...] or [NONE] + +⚠️ STEP 4: CONTENT ASSESSMENT (ONLY FOR MATCHING KEYS) +For keys that exist in both (from Step 2), assess content: +Key 'KeyName': Content assessment [IDENTICAL/SIMILAR/DIFFERENT] +[Only assess keys that showed YES in Step 2] + +⚠️ STEP 5: MECHANICAL SCORING +Apply these exact penalties: +- Missing key (not in actual): 100/N points each +- Similar key (exists with similar content): 50/N points each +- Wrong key (exists but SIGNIFICANTLY different content): 100/N points each +- Identical key (exists with IDENTICAL content): 0 points each +- Extra key (in actual but not expected): 10/N points each + +⚠️ MECHANICAL CATEGORIZATION: +Based on Steps 1-4, categorize each expected key: + +1. 'ExpectedKey1' → [MISSING/WRONG/SIMILAR/IDENTICAL] → Penalty: [calculation] +2. 'ExpectedKey2' → [MISSING/WRONG/SIMILAR/IDENTICAL] → Penalty: [calculation] +[Continue for all expected keys] + +Extra keys: [count] × (10/N) = [calculation] + +⚠️ EXACT ARITHMETIC: +Penalty calculations (show all work): +- N = [number] +- Missing keys: [count] × (100/[N]) = [count] × [decimal] = [total] +- Wrong keys: [count] × (100/[N]) = [count] × [decimal] = [total] +- Similar keys: [count] × (50/[N]) = [count] × [decimal] = [total] +- Extra keys: [count] × (10/[N]) = [count] × [decimal] = [total] + +Total penalty: [sum all penalties] = [final penalty] +Final score: 100 - [final penalty] = [score] (minimum 0) + +⚠️ VERIFICATION CHECKLIST: +- Did I count N correctly by listing all expected keys? +- Did I check EXACT key name matches (character-for-character)? +- Did I only assess content for keys that exist in both? +- Did I calculate exact penalty fractions (100/N, not 100)? +- Did I show all arithmetic work step by step? +- Is my final score between 0 and 100? + +⚠️ CRITICAL RULES FOR CONSISTENCY: +- NEVER use semantic interpretation for key names (must be exact match) +- NEVER assess content for missing keys +- ALWAYS calculate penalties as fractions of N +- ALWAYS show exact arithmetic work +- IDENTICAL inputs MUST produce IDENTICAL outputs. + +⚠️ DETERMINISTIC REQUIREMENTS: +• Key matching is purely textual (character-by-character comparison) +• Content assessment is only for keys that exist in both outputs +• All arithmetic must be shown with exact fractions""" + + LLM_JUDGE_STRICT_JSON_SIMILARITY_DEFAULT_USER_PROMPT = """ExpectedOutput (ground truth):\n{{ExpectedOutput}}\n\nActualOutput (model answer):\n{{ActualOutput}}""" + + LLM_JUDGE_SIMULATION_TRAJECTORY_SYSTEM_PROMPT = """You are an expert evaluator tasked with assessing an agent running through a simulation. +The simulation engine was used to mock the tool responses given during the agent run based on the simulation instructions. +The agent did not know that the tool responses are simulated. +You will be given: +1. The instructions the simulation engine was given to mock the tool responses given during the agent run. +2. Expected behavior for the agent during the simulation. +3. A trace/history of the agent run. +4. The agent configuration used during the run. +Your task is to carefully analyze the agent run trace and it's output according to the specified criterion. +0 means the agent did not meet the criterion at all. +100 means the agent fully met the criterion. +To submit your evaluation, use the correct tool for the score type. +Never answer using text. Only use the tool to submit your score. +""" + + LLM_JUDGE_SIMULATION_TRAJECTORY_DEFAULT_USER_PROMPT = """As an expert evaluator, determine how well the agent did on a scale of 0-100. Focus on if the simulation was successful and if the agent behaved according to the expected output accounting for alternative valid expressions, and reasonable variations in language while maintaining high standards for accuracy and completeness. Provide your score with a justification, explaining briefly and concisely why you gave that score. +---- +AgentInput: +{{UserOrSyntheticInput}} +---- +SimulationInstructions: +{{SimulationInstructions}} +---- +ExpectedAgentBehavior: +{{ExpectedAgentBehavior}} +---- +AgentRunHistory: +{{AgentRunHistory}} +""" + + LLM_JUDGE_TRAJECTORY_SYSTEM_PROMPT = """You are an expert evaluator tasked with assessing an agent's behavior based on its execution trajectory in a simulation or real environment. +You will be given: +1. Expected behavior for the agent during the run. +2. A trace/history of the agent's actions and outputs. +3. The agent configuration used during the run. +Your task is to carefully analyze the agent's trajectory and output according to the specified criterion. +A score of 0 means the agent did not meet the criterion at all, while 100 means the agent fully met the criterion. +To submit your evaluation, use the correct tool for the score type. +Never answer using text. Only use the tool to submit your score. +""" + + LLM_JUDGE_TRAJECTORY_DEFAULT_USER_PROMPT = """As an expert evaluator, determine how well the agent performed on a scale of 0-100. Focus on whether the agent's actions and outputs matched the expected behavior, while allowing for alternative valid expressions and reasonable variations in language. Maintain high standards for accuracy and completeness. Provide your score with a brief and clear justification explaining your reasoning. +---- +AgentInput: +{{UserOrSyntheticInput}} +---- +ExpectedAgentBehavior: +{{ExpectedAgentBehavior}} +---- +AgentRunHistory: +{{AgentRunHistory}} +""" diff --git a/packages/uipath-eval/src/uipath_eval/models/models.py b/packages/uipath-eval/src/uipath_eval/models/models.py new file mode 100644 index 000000000..729ea4aea --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/models/models.py @@ -0,0 +1,378 @@ +"""Models for evaluation framework including execution data and evaluation results.""" + +import traceback +from dataclasses import dataclass +from enum import Enum, IntEnum +from typing import Annotated, Any, Literal, Union + +from opentelemetry.sdk.trace import ReadableSpan +from pydantic import BaseModel, ConfigDict, Field, model_serializer +from pydantic.alias_generators import to_camel +from pydantic_core import core_schema + + +class AgentExecution(BaseModel): + """Represents the execution data of an agent for evaluation purposes.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + agent_input: dict[str, Any] | None + agent_output: dict[str, Any] | str + agent_trace: list[ReadableSpan] + expected_agent_behavior: str | None = None + simulation_instructions: str = "" + + +class LLMResponse(BaseModel): + """Response from an LLM evaluator.""" + + score: float + justification: str + + +class ScoreType(IntEnum): + """Types of evaluation scores.""" + + BOOLEAN = 0 + NUMERICAL = 1 + ERROR = 2 + + +class BaseEvaluationResult(BaseModel): + """Base class for evaluation results.""" + + details: str | BaseModel | None = None + # this is marked as optional, as it is populated inside the 'measure_execution_time' decorator + evaluation_time: float | None = None + + +class BooleanEvaluationResult(BaseEvaluationResult): + """Result of a boolean evaluation.""" + + score: bool + score_type: Literal[ScoreType.BOOLEAN] = ScoreType.BOOLEAN + + +class NumericEvaluationResult(BaseEvaluationResult): + """Result of a numerical evaluation.""" + + score: float + score_type: Literal[ScoreType.NUMERICAL] = ScoreType.NUMERICAL + + +class ErrorEvaluationResult(BaseEvaluationResult): + """Result of an error evaluation.""" + + score: float = 0.0 + score_type: Literal[ScoreType.ERROR] = ScoreType.ERROR + + +EvaluationResult = Annotated[ + Union[BooleanEvaluationResult, NumericEvaluationResult, ErrorEvaluationResult], + Field(discriminator="score_type"), +] + + +class EvaluationResultDto(BaseModel): + """Serializable evaluation result used for aggregation and transport.""" + + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) + + score: float + details: str | dict[str, Any] | None = None + evaluation_time: float | None = None + + @model_serializer(mode="wrap") + def serialize_model( + self, + serializer: core_schema.SerializerFunctionWrapHandler, + info: core_schema.SerializationInfo, + ) -> Any: + """Omit 'details' key from serialized output when it is None.""" + data = serializer(self) + if self.details is None and isinstance(data, dict): + data.pop("details", None) + return data + + @classmethod + def from_evaluation_result( + cls, evaluation_result: EvaluationResult + ) -> "EvaluationResultDto": + """Convert an EvaluationResult to a serializable DTO.""" + score_type = evaluation_result.score_type + score: float + if score_type == ScoreType.BOOLEAN: + score = 100 if evaluation_result.score else 0 + elif score_type == ScoreType.ERROR: + score = 0 + else: + score = evaluation_result.score + + # Convert BaseModel details to dict so Pydantic doesn't lose subclass fields + if isinstance(evaluation_result.details, BaseModel): + details: str | dict[str, Any] | None = ( + evaluation_result.details.model_dump() + ) + else: + details = evaluation_result.details + + return cls( + score=score, + details=details, + evaluation_time=evaluation_result.evaluation_time, + ) + + +class EvalItemResult(BaseModel): + """Result of a single evaluation item.""" + + evaluator_id: str + result: EvaluationResult + + +class LegacyEvaluatorCategory(IntEnum): + """Types of evaluators.""" + + Deterministic = 0 + LlmAsAJudge = 1 + AgentScorer = 2 + Trajectory = 3 + + @classmethod + def from_int(cls, value: int) -> "LegacyEvaluatorCategory": + """Construct EvaluatorCategory from an int value.""" + if value in cls._value2member_map_: + return cls(value) + else: + raise ValueError(f"{value} is not a valid EvaluatorCategory value") + + +class LegacyEvaluatorType(IntEnum): + """Subtypes of evaluators.""" + + Unknown = 0 + Equals = 1 + Contains = 2 + Regex = 3 + Factuality = 4 + Custom = 5 + JsonSimilarity = 6 + Trajectory = 7 + ContextPrecision = 8 + Faithfulness = 9 + CSVColumnExactMatch = 10 + + @classmethod + def from_int(cls, value: int) -> "LegacyEvaluatorType": + """Construct EvaluatorCategory from an int value.""" + if value in cls._value2member_map_: + return cls(value) + else: + raise ValueError(f"{value} is not a valid EvaluatorType value") + + +@dataclass +class TrajectoryEvaluationSpan: + """Simplified span representation for trajectory evaluation. + + Contains span information needed for evaluating agent execution paths, + excluding timestamps which are not useful for trajectory analysis. + """ + + name: str + status: str + attributes: dict[str, Any] + parent_name: str | None = None + events: list[dict[str, Any]] | None = None + + def __post_init__(self): + """Initialize default values.""" + if self.events is None: + self.events = [] + + @classmethod + def from_readable_span( + cls, span: ReadableSpan, parent_spans: dict[int, str] | None = None + ) -> "TrajectoryEvaluationSpan": + """Convert a ReadableSpan to a TrajectoryEvaluationSpan. + + Args: + span: The OpenTelemetry ReadableSpan to convert + parent_spans: Optional mapping of span IDs to names for parent lookup + + Returns: + TrajectoryEvaluationSpan with relevant data extracted + """ + # Extract status + status_map = {0: "unset", 1: "ok", 2: "error"} + status = status_map.get(span.status.status_code.value, "unknown") + + # Extract attributes - keep all attributes for now + attributes = {} + if span.attributes: + attributes = dict(span.attributes) + + # Get parent name if available + parent_name = None + if span.parent and parent_spans and span.parent.span_id in parent_spans: + parent_name = parent_spans[span.parent.span_id] + + # Extract events (without timestamps) + events = [] + if hasattr(span, "events") and span.events: + for event in span.events: + event_data = { + "name": event.name, + "attributes": dict(event.attributes) if event.attributes else {}, + } + events.append(event_data) + + return cls( + name=span.name, + status=status, + attributes=attributes, + parent_name=parent_name, + events=events, + ) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for JSON serialization.""" + return { + "name": self.name, + "status": self.status, + "parent_name": self.parent_name, + "attributes": self.attributes, + "events": self.events, + } + + +class TrajectoryEvaluationTrace(BaseModel): + """Container for a collection of trajectory evaluation spans.""" + + spans: list[TrajectoryEvaluationSpan] + + @classmethod + def from_readable_spans( + cls, spans: list[ReadableSpan] + ) -> "TrajectoryEvaluationTrace": + """Convert a list of ReadableSpans to TrajectoryEvaluationTrace. + + Args: + spans: List of OpenTelemetry ReadableSpans to convert + + Returns: + TrajectoryEvaluationTrace with converted spans + """ + # Create a mapping of span IDs to names for parent lookup + span_id_to_name = { + ctx.span_id: span.name + for span in spans + if (ctx := span.get_span_context()) is not None + } + + evaluation_spans = [ + TrajectoryEvaluationSpan.from_readable_span(span, span_id_to_name) + for span in spans + ] + + return cls(spans=evaluation_spans) + + model_config = ConfigDict(arbitrary_types_allowed=True) + + +class EvaluatorType(str, Enum): + """Evaluator type.""" + + CONTAINS = "uipath-contains" + EXACT_MATCH = "uipath-exact-match" + JSON_SIMILARITY = "uipath-json-similarity" + LLM_JUDGE_OUTPUT_SEMANTIC_SIMILARITY = "uipath-llm-judge-output-semantic-similarity" + LLM_JUDGE_OUTPUT_STRICT_JSON_SIMILARITY = ( + "uipath-llm-judge-output-strict-json-similarity" + ) + LLM_JUDGE_TRAJECTORY_SIMILARITY = "uipath-llm-judge-trajectory-similarity" + LLM_JUDGE_TRAJECTORY_SIMULATION = "uipath-llm-judge-trajectory-simulation" + LLM_JUDGE_TRAJECTORY = "uipath-llm-judge-trajectory" + LLM_JUDGE_OUTPUT = "uipath-llm-judge-output" + TOOL_CALL_ARGS = "uipath-tool-call-args" + TOOL_CALL_COUNT = "uipath-tool-call-count" + TOOL_CALL_ORDER = "uipath-tool-call-order" + TOOL_CALL_OUTPUT = "uipath-tool-call-output" + BINARY_CLASSIFICATION = "uipath-binary-classification" + MULTICLASS_CLASSIFICATION = "uipath-multiclass-classification" + + +class ToolCall(BaseModel): + """Represents a tool call with its arguments.""" + + name: str + args: dict[str, Any] + + +class ToolOutput(BaseModel): + """Represents a tool output with its output.""" + + name: str + output: str + + +class UiPathEvaluationErrorCategory(str, Enum): + """Categories of evaluation errors.""" + + SYSTEM = "System" + USER = "User" + UNKNOWN = "Unknown" + + +class UiPathEvaluationErrorContract(BaseModel): + """Standard error contract used across the runtime.""" + + code: str # Human-readable code uniquely identifying this error type across the platform. + # Format: . (e.g. LangGraph.InvaliGraphReference) + # Only use alphanumeric characters [A-Za-z0-9] and periods. No whitespace allowed. + + title: str # Short, human-readable summary of the problem that should remain consistent + # across occurrences. + + detail: ( + str # Human-readable explanation specific to this occurrence of the problem. + ) + # May include context, recommended actions, or technical details like call stacks + # for technical users. + + category: UiPathEvaluationErrorCategory = UiPathEvaluationErrorCategory.UNKNOWN + + +class UiPathEvaluationError(Exception): + """Base exception class for UiPath evaluation errors with structured error information.""" + + def __init__( + self, + code: str, + title: str, + detail: str, + category: UiPathEvaluationErrorCategory = UiPathEvaluationErrorCategory.UNKNOWN, + prefix: str = "Python", + include_traceback: bool = True, + ): + """Initialize the UiPathEvaluationError.""" + # Get the current traceback as a string + if include_traceback: + tb = traceback.format_exc() + if ( + tb and tb.strip() != "NoneType: None" + ): # Ensure there's an actual traceback + detail = f"{detail}\n\n{tb}" + + self.error_info = UiPathEvaluationErrorContract( + code=f"{prefix}.{code}", + title=title, + detail=detail, + category=category, + ) + super().__init__(detail) + + @property + def as_dict(self) -> dict[str, Any]: + """Get the error information as a dictionary.""" + return self.error_info.model_dump() diff --git a/packages/uipath-eval/src/uipath_eval/py.typed b/packages/uipath-eval/src/uipath_eval/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/uipath-eval/src/uipath_eval/runtime/__init__.py b/packages/uipath-eval/src/uipath_eval/runtime/__init__.py new file mode 100644 index 000000000..36aad4d9f --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/runtime/__init__.py @@ -0,0 +1,9 @@ +"""Pure runtime utilities for evaluation — asyncio and stdlib only. + +Note: UiPathEvalRuntime, UiPathEvalContext, and evaluate() are NOT here. +Those depend on uipath.runtime and stay in uipath.eval.runtime. +This module only exposes the clean, dependency-free utilities. +""" + +from uipath_eval.runtime._parallelization import * # noqa: F401, F403 +from uipath_eval.runtime._utils import * # noqa: F401, F403 diff --git a/packages/uipath-eval/src/uipath_eval/runtime/_parallelization.py b/packages/uipath-eval/src/uipath_eval/runtime/_parallelization.py new file mode 100644 index 000000000..fc2ea32c2 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/runtime/_parallelization.py @@ -0,0 +1,73 @@ +import asyncio +import logging +from typing import Awaitable, Iterable, TypeVar + +logger = logging.getLogger(__name__) + +T = TypeVar("T") + + +async def execute_parallel( + evaluation_result_iterable: Iterable[Awaitable[T]], + workers: int, +) -> list[T]: + # Create a queue with max concurrency + queue: asyncio.Queue[tuple[int, Awaitable[T]] | None] = asyncio.Queue( + maxsize=workers + ) + + # Dictionary to store results with their original indices + results_dict: dict[int, T] = {} + errors: dict[int, BaseException] = {} + + # Producer task to fill the queue + async def producer() -> None: + for index, eval_item in enumerate(evaluation_result_iterable): + await queue.put((index, eval_item)) + # Signal completion by putting None markers + for _ in range(workers): + await queue.put(None) + + # Worker function to process items from the queue + async def worker(worker_id: int) -> None: + while True: + item = await queue.get() + + # Check for termination signal + if item is None: + queue.task_done() + break + + index, eval_item = item + + try: + # Execute the evaluation + result = await eval_item + + # Store result with its index to maintain order + results_dict[index] = result + except Exception as e: + logger.warning("Evaluation item %d failed: %s", index, e) + errors[index] = e + finally: + # Mark the task as done regardless of outcome + queue.task_done() + + # Start producer + producer_task = asyncio.create_task(producer()) + + # Create worker tasks based on workers + worker_tasks = [asyncio.create_task(worker(i)) for i in range(workers)] + + # Wait for producer and all workers to complete + await producer_task + await asyncio.gather(*worker_tasks) + + if errors: + first_error = next(iter(errors.values())) + raise RuntimeError( + f"{len(errors)} evaluation(s) failed. First error: {first_error}" + ) from first_error + + # Return results in the original order + return [results_dict[i] for i in range(len(results_dict))] diff --git a/packages/uipath-eval/src/uipath_eval/runtime/_utils.py b/packages/uipath-eval/src/uipath_eval/runtime/_utils.py new file mode 100644 index 000000000..1c2d43597 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/runtime/_utils.py @@ -0,0 +1,80 @@ +"""Utility functions for applying input overrides to evaluation inputs.""" + +import copy +import logging +from typing import Any + +logger = logging.getLogger(__name__) + + +def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]: + """Recursively merge override into base dictionary. + + Args: + base: The base dictionary to merge into + override: The override dictionary to merge from + + Returns: + A new dictionary with overrides recursively merged into base + """ + result = copy.deepcopy(base) + for key, value in override.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + # Recursively merge nested dicts + result[key] = deep_merge(result[key], value) + else: + # Direct replacement for non-dict or new keys + result[key] = value + return result + + +def apply_input_overrides( + inputs: dict[str, Any], + input_overrides: dict[str, Any], + eval_id: str | None = None, +) -> dict[str, Any]: + """Apply input overrides to inputs using direct field override. + + Format: Per-evaluation overrides (keys are evaluation IDs): + {"eval-1": {"operator": "*"}, "eval-2": {"a": 100}} + + Deep merge is supported for nested objects: + - {"filePath": {"ID": "new-id"}} - deep merges inputs["filePath"] with {"ID": "new-id"} + + Args: + inputs: The original inputs dictionary + input_overrides: Dictionary mapping evaluation IDs to their override values + eval_id: The evaluation ID (required) + + Returns: + A new dictionary with overrides applied + """ + if not input_overrides: + return inputs + + if not eval_id: + logger.warning( + "eval_id not provided, cannot apply input overrides. Input overrides require eval_id." + ) + return inputs + + result = copy.deepcopy(inputs) + + # Check if there are overrides for this specific eval_id + if eval_id not in input_overrides: + logger.debug(f"No overrides found for eval_id='{eval_id}'") + return result + + overrides_to_apply = input_overrides[eval_id] + logger.debug(f"Applying overrides for eval_id='{eval_id}': {overrides_to_apply}") + + # Apply direct field overrides with recursive deep merge + for key, value in overrides_to_apply.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + # Recursive deep merge for dict values + result[key] = deep_merge(result[key], value) + else: + # Direct replacement for non-dict or new keys + result[key] = value + + return result diff --git a/packages/uipath-eval/src/uipath_eval/runtime/events.py b/packages/uipath-eval/src/uipath_eval/runtime/events.py new file mode 100644 index 000000000..589f82ba7 --- /dev/null +++ b/packages/uipath-eval/src/uipath_eval/runtime/events.py @@ -0,0 +1,88 @@ +"""Evaluation runtime events.""" + +import logging +from enum import Enum +from typing import Any, Union + +from opentelemetry.sdk.trace import ReadableSpan +from pydantic import BaseModel, ConfigDict, SkipValidation, model_validator + +from ..evaluators.base_evaluator import GenericBaseEvaluator +from ..models import EvalItemResult +from ..models.evaluation_set import EvaluationItem + + +class EvaluationEvents(str, Enum): + """Event types for evaluation runs.""" + + CREATE_EVAL_SET_RUN = "create_eval_set_run" + CREATE_EVAL_RUN = "create_eval_run" + UPDATE_EVAL_SET_RUN = "update_eval_set_run" + UPDATE_EVAL_RUN = "update_eval_run" + + +class EvalSetRunCreatedEvent(BaseModel): + """Event emitted when an evaluation set run is created.""" + + execution_id: str + entrypoint: str + eval_set_id: str + eval_set_run_id: str | None = None + no_of_evals: int + # skip validation to avoid abstract class instantiation + evaluators: SkipValidation[list[GenericBaseEvaluator[Any, Any, Any]]] + + +class EvalRunCreatedEvent(BaseModel): + """Event emitted when an individual evaluation run is created.""" + + execution_id: str + eval_item: EvaluationItem + + +class EvalItemExceptionDetails(BaseModel): + """Details of an exception that occurred during an evaluation item.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + runtime_exception: bool = False + exception: Exception + + +class EvalRunUpdatedEvent(BaseModel): + """Event emitted when an individual evaluation run is updated with results.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + execution_id: str + eval_item: EvaluationItem + eval_results: list[EvalItemResult] + success: bool + agent_output: Any + agent_execution_time: float + spans: list[ReadableSpan] + logs: list[logging.LogRecord] + exception_details: EvalItemExceptionDetails | None = None + + @model_validator(mode="after") + def validate_exception_details(self): + """Ensure that exception details are provided when success is False.""" + if not self.success and self.exception_details is None: + raise ValueError("exception_details must be provided when success is False") + return self + + +class EvalSetRunUpdatedEvent(BaseModel): + """Event emitted when an evaluation set run is updated.""" + + execution_id: str + evaluator_scores: dict[str, float] + success: bool = True + + +ProgressEvent = Union[ + EvalSetRunCreatedEvent, + EvalRunCreatedEvent, + EvalRunUpdatedEvent, + EvalSetRunUpdatedEvent, +] diff --git a/packages/uipath-eval/tests/__init__.py b/packages/uipath-eval/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/uipath-eval/tests/test_evaluators.py b/packages/uipath-eval/tests/test_evaluators.py new file mode 100644 index 000000000..01c0d45b3 --- /dev/null +++ b/packages/uipath-eval/tests/test_evaluators.py @@ -0,0 +1,206 @@ +"""Tests for uipath_eval evaluators.""" + +import uuid + +import pytest + +from uipath_eval.evaluators.contains_evaluator import ( + ContainsEvaluationCriteria, + ContainsEvaluator, + ContainsEvaluatorConfig, +) +from uipath_eval.evaluators.exact_match_evaluator import ( + ExactMatchEvaluator, + ExactMatchEvaluatorConfig, +) +from uipath_eval.evaluators.json_similarity_evaluator import ( + JsonSimilarityEvaluator, + JsonSimilarityEvaluatorConfig, +) +from uipath_eval.evaluators.output_evaluator import OutputEvaluationCriteria +from uipath_eval.models.models import AgentExecution + + +def _id(): + return str(uuid.uuid4()) + + +def _make_execution(output, agent_input=None): + return AgentExecution( + agent_input=agent_input or {}, + agent_output=output, + agent_trace=[], + ) + + +class TestExactMatchEvaluator: + def _make(self, case_sensitive=False, negated=False): + return ExactMatchEvaluator( + id=_id(), + evaluatorConfig=ExactMatchEvaluatorConfig( + case_sensitive=case_sensitive, + negated=negated, + ), + ) + + def _criteria(self, expected): + return OutputEvaluationCriteria(expected_output=expected) + + @pytest.mark.asyncio + async def test_exact_dict_match(self): + result = await self._make().evaluate( + _make_execution({"key": "value"}), self._criteria({"key": "value"}) + ) + assert result.score == 1.0 + + @pytest.mark.asyncio + async def test_dict_mismatch(self): + result = await self._make().evaluate( + _make_execution({"key": "wrong"}), self._criteria({"key": "value"}) + ) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_string_case_insensitive(self): + result = await self._make(case_sensitive=False).evaluate( + _make_execution("Hello World"), self._criteria("hello world") + ) + assert result.score == 1.0 + + @pytest.mark.asyncio + async def test_string_case_sensitive_mismatch(self): + result = await self._make(case_sensitive=True).evaluate( + _make_execution("Hello World"), self._criteria("hello world") + ) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_negated_match_returns_zero(self): + result = await self._make(negated=True).evaluate( + _make_execution("same"), self._criteria("same") + ) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_negated_mismatch_returns_one(self): + result = await self._make(negated=True).evaluate( + _make_execution("different"), self._criteria("same") + ) + assert result.score == 1.0 + + @pytest.mark.asyncio + async def test_numeric_normalization(self): + result = await self._make().evaluate( + _make_execution({"score": 1}), self._criteria({"score": 1.0}) + ) + assert result.score == 1.0 + + +class TestContainsEvaluator: + def _make(self, case_sensitive=False, negated=False): + return ContainsEvaluator( + id=_id(), + evaluatorConfig=ContainsEvaluatorConfig( + case_sensitive=case_sensitive, + negated=negated, + ), + ) + + def _criteria(self, text): + return ContainsEvaluationCriteria(search_text=text) + + @pytest.mark.asyncio + async def test_contains_substring(self): + result = await self._make().evaluate( + _make_execution("The quick brown fox"), self._criteria("brown") + ) + assert result.score == 1.0 + + @pytest.mark.asyncio + async def test_does_not_contain(self): + result = await self._make().evaluate( + _make_execution("The quick brown fox"), self._criteria("cat") + ) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_case_insensitive_match(self): + result = await self._make(case_sensitive=False).evaluate( + _make_execution("Hello World"), self._criteria("hello") + ) + assert result.score == 1.0 + + @pytest.mark.asyncio + async def test_case_sensitive_no_match(self): + result = await self._make(case_sensitive=True).evaluate( + _make_execution("Hello World"), self._criteria("hello") + ) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_negated_contains_returns_zero(self): + result = await self._make(negated=True).evaluate( + _make_execution("The quick brown fox"), self._criteria("brown") + ) + assert result.score == 0.0 + + @pytest.mark.asyncio + async def test_negated_missing_returns_one(self): + result = await self._make(negated=True).evaluate( + _make_execution("The quick brown fox"), self._criteria("cat") + ) + assert result.score == 1.0 + + +class TestJsonSimilarityEvaluator: + def _make(self): + return JsonSimilarityEvaluator( + id=_id(), + evaluatorConfig=JsonSimilarityEvaluatorConfig(), + ) + + def _criteria(self, expected): + return OutputEvaluationCriteria(expected_output=expected) + + @pytest.mark.asyncio + async def test_identical_dicts_score_one(self): + result = await self._make().evaluate( + _make_execution({"a": 1, "b": "hello"}), + self._criteria({"a": 1, "b": "hello"}), + ) + assert result.score == pytest.approx(1.0, abs=1e-6) + + @pytest.mark.asyncio + async def test_completely_different_score_zero(self): + result = await self._make().evaluate( + _make_execution({"x": "foo"}), self._criteria({"a": "bar"}) + ) + assert result.score == pytest.approx(0.0, abs=1e-6) + + @pytest.mark.asyncio + async def test_partial_match_between_zero_and_one(self): + result = await self._make().evaluate( + _make_execution({"a": 1, "b": "wrong"}), + self._criteria({"a": 1, "b": "right"}), + ) + assert 0.0 < result.score < 1.0 + + @pytest.mark.asyncio + async def test_numeric_tolerance(self): + result = await self._make().evaluate( + _make_execution({"v": 10.0}), self._criteria({"v": 10}) + ) + assert result.score == pytest.approx(1.0, abs=1e-6) + + @pytest.mark.asyncio + async def test_empty_expected_score_one(self): + result = await self._make().evaluate(_make_execution({}), self._criteria({})) + assert result.score == pytest.approx(1.0, abs=1e-6) + + @pytest.mark.asyncio + async def test_score_clamped(self): + result = await self._make().evaluate( + _make_execution({"a": "completely different text"}), + self._criteria({"a": "xyz"}), + ) + assert 0.0 <= result.score <= 1.0 diff --git a/packages/uipath-eval/uv.lock b/packages/uipath-eval/uv.lock new file mode 100644 index 000000000..ab148b4b0 --- /dev/null +++ b/packages/uipath-eval/uv.lock @@ -0,0 +1,1718 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jiter" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" }, + { url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" }, + { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, + { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, + { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, + { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, + { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, + { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, + { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, + { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, + { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, + { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, + { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, + { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, + { url = "https://files.pythonhosted.org/packages/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/fe/20190232d9b513242899dbb0c2bb77e31b4d61e343743adbe90ebc2603d2/langchain_core-1.3.0.tar.gz", hash = "sha256:14a39f528bf459aa3aa40d0a7f7f1bae7520d435ef991ae14a4ceb74d8c49046", size = 860755, upload-time = "2026-04-17T14:51:38.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/e2/dbfa347aa072a6dc4cd38d6f9ebfc730b4c14c258c47f480f4c5c546f177/langchain_core-1.3.0-py3-none-any.whl", hash = "sha256:baf16ee028475df177b9ab8869a751c79406d64a6f12125b93802991b566cced", size = 515140, upload-time = "2026-04-17T14:51:36.274Z" }, +] + +[[package]] +name = "langsmith" +version = "0.7.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "uuid-utils" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/75/1ee27b3510bf5b1b569b9695c9466c256caab45885bd569c0c67720236ad/langsmith-0.7.33.tar.gz", hash = "sha256:fa2d81ad6e8374a81fda9291894f6fcae714e55fbf11a0b07578e3cd4b1ea384", size = 1186298, upload-time = "2026-04-20T16:17:54.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/76/53033db34ffccd25d62c32b23b9468f7228b455da6976e1c420ae31555c4/langsmith-0.7.33-py3-none-any.whl", hash = "sha256:5b535b991d52d3b664ebb8dc6f95afcf8d0acb42e062ac45a54a6a4820139f20", size = 378981, upload-time = "2026-04-20T16:17:52.503Z" }, +] + +[[package]] +name = "librt" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/1e/2ec7afcebcf3efea593d13aee18bbcfdd3a243043d848ebf385055e9f636/librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671", size = 67155, upload-time = "2026-04-09T16:04:42.933Z" }, + { url = "https://files.pythonhosted.org/packages/18/77/72b85afd4435268338ad4ec6231b3da8c77363f212a0227c1ff3b45e4d35/librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d", size = 69916, upload-time = "2026-04-09T16:04:44.042Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/948ea0204fbe2e78add6d46b48330e58d39897e425560674aee302dca81c/librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6", size = 199635, upload-time = "2026-04-09T16:04:45.5Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cd/894a29e251b296a27957856804cfd21e93c194aa131de8bb8032021be07e/librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1", size = 211051, upload-time = "2026-04-09T16:04:47.016Z" }, + { url = "https://files.pythonhosted.org/packages/18/8f/dcaed0bc084a35f3721ff2d081158db569d2c57ea07d35623ddaca5cfc8e/librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882", size = 224031, upload-time = "2026-04-09T16:04:48.207Z" }, + { url = "https://files.pythonhosted.org/packages/03/44/88f6c1ed1132cd418601cc041fbd92fed28b3a09f39de81978e0822d13ff/librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990", size = 218069, upload-time = "2026-04-09T16:04:50.025Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/7d02e981c2db12188d82b4410ff3e35bfdb844b26aecd02233626f46af2b/librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4", size = 224857, upload-time = "2026-04-09T16:04:51.684Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/c77e706b7215ca32e928d47535cf13dbc3d25f096f84ddf8fbc06693e229/librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb", size = 219865, upload-time = "2026-04-09T16:04:52.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/d1/32b0c1a0eb8461c70c11656c46a29f760b7c7edf3c36d6f102470c17170f/librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076", size = 218451, upload-time = "2026-04-09T16:04:54.174Z" }, + { url = "https://files.pythonhosted.org/packages/74/d1/adfd0f9c44761b1d49b1bec66173389834c33ee2bd3c7fd2e2367f1942d4/librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a", size = 241300, upload-time = "2026-04-09T16:04:55.452Z" }, + { url = "https://files.pythonhosted.org/packages/09/b0/9074b64407712f0003c27f5b1d7655d1438979155f049720e8a1abd9b1a1/librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6", size = 55668, upload-time = "2026-04-09T16:04:56.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/19/40b77b77ce80b9389fb03971431b09b6b913911c38d412059e0b3e2a9ef2/librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8", size = 62976, upload-time = "2026-04-09T16:04:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/70/9d/9fa7a64041e29035cb8c575af5f0e3840be1b97b4c4d9061e0713f171849/librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a", size = 53502, upload-time = "2026-04-09T16:04:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/3d/5b373635b3146264eb7a68d09e5ca11c305bbb058dfffbb47c47daf4f632/mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804", size = 3815892, upload-time = "2026-04-13T02:46:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/0d/555ab7453cc4a4a8643b7f21c842b1a84c36b15392061ae7b052ee119320/mypy-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c01eb9bac2c6a962d00f9d23421cd2913840e65bba365167d057bd0b4171a92e", size = 14336012, upload-time = "2026-04-13T02:45:39.935Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/85a28893f7db8a16ebb41d1e9dfcb4475844d06a88480b6639e32a74d6ef/mypy-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55d12ddbd8a9cac5b276878bd534fa39fff5bf543dc6ae18f25d30c8d7d27fca", size = 13224636, upload-time = "2026-04-13T02:45:49.659Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/bd4cd3c2caeb6c448b669222b8cfcbdee4a03b89431527b56fca9e56b6f3/mypy-1.20.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0aa322c1468b6cdfc927a44ce130f79bb44bcd34eb4a009eb9f96571fd80955", size = 13663471, upload-time = "2026-04-13T02:46:20.276Z" }, + { url = "https://files.pythonhosted.org/packages/3e/56/7ee8c471e10402d64b6517ae10434541baca053cffd81090e4097d5609d4/mypy-1.20.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f8bc95899cf676b6e2285779a08a998cc3a7b26f1026752df9d2741df3c79e8", size = 14532344, upload-time = "2026-04-13T02:46:44.205Z" }, + { url = "https://files.pythonhosted.org/packages/b5/95/b37d1fa859a433f6156742e12f62b0bb75af658544fb6dada9363918743a/mypy-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:47c2b90191a870a04041e910277494b0d92f0711be9e524d45c074fe60c00b65", size = 14776670, upload-time = "2026-04-13T02:45:52.481Z" }, + { url = "https://files.pythonhosted.org/packages/03/77/b302e4cb0b80d2bdf6bf4fce5864bb4cbfa461f7099cea544eaf2457df78/mypy-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:9857dc8d2ec1a392ffbda518075beb00ac58859979c79f9e6bdcb7277082c2f2", size = 10816524, upload-time = "2026-04-13T02:45:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/7f/21/d969d7a68eb964993ebcc6170d5ecaf0cf65830c58ac3344562e16dc42a9/mypy-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:09d8df92bb25b6065ab91b178da843dda67b33eb819321679a6e98a907ce0e10", size = 9750419, upload-time = "2026-04-13T02:45:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/69/1b/75a7c825a02781ca10bc2f2f12fba2af5202f6d6005aad8d2d1f264d8d78/mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51", size = 14494077, upload-time = "2026-04-13T02:45:55.085Z" }, + { url = "https://files.pythonhosted.org/packages/b0/54/5e5a569ea5c2b4d48b729fb32aa936eeb4246e4fc3e6f5b3d36a2dfbefb9/mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28", size = 13319495, upload-time = "2026-04-13T02:45:29.674Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a4/a1945b19f33e91721b59deee3abb484f2fa5922adc33bb166daf5325d76d/mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f", size = 13696948, upload-time = "2026-04-13T02:46:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/75e969781c2359b2f9c15b061f28ec6d67c8b61865ceda176e85c8e7f2de/mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37", size = 14706744, upload-time = "2026-04-13T02:46:00.482Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6e/b221b1de981fc4262fe3e0bf9ec272d292dfe42394a689c2d49765c144c4/mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237", size = 14949035, upload-time = "2026-04-13T02:45:06.021Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4b/298ba2de0aafc0da3ff2288da06884aae7ba6489bc247c933f87847c41b3/mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d", size = 10883216, upload-time = "2026-04-13T02:45:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f9/5e25b8f0b8cb92f080bfed9c21d3279b2a0b6a601cdca369a039ba84789d/mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019", size = 9814299, upload-time = "2026-04-13T02:45:21.934Z" }, + { url = "https://files.pythonhosted.org/packages/21/e8/ef0991aa24c8f225df10b034f3c2681213cb54cf247623c6dec9a5744e70/mypy-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f3886c03e40afefd327bd70b3f634b39ea82e87f314edaa4d0cce4b927ddcc1", size = 14500739, upload-time = "2026-04-13T02:46:05.442Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/416ebec3047636ed89fa871dc8c54bf05e9e20aa9499da59790d7adb312d/mypy-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e860eb3904f9764e83bafd70c8250bdffdc7dde6b82f486e8156348bf7ceb184", size = 13314735, upload-time = "2026-04-13T02:46:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/10/1e/1505022d9c9ac2e014a384eb17638fb37bf8e9d0a833ea60605b66f8f7ba/mypy-1.20.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4b5aac6e785719da51a84f5d09e9e843d473170a9045b1ea7ea1af86225df4b", size = 13704356, upload-time = "2026-04-13T02:45:19.773Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/275b01f5eba5c467a3318ec214dd865abb66e9c811231c8587287b92876a/mypy-1.20.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f37b6cd0fe2ad3a20f05ace48ca3523fc52ff86940e34937b439613b6854472e", size = 14696420, upload-time = "2026-04-13T02:45:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/a1/57/b3779e134e1b7250d05f874252780d0a88c068bc054bcff99ca20a3a2986/mypy-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4bbb0f6b54ce7cc350ef4a770650d15fa70edd99ad5267e227133eda9c94218", size = 14936093, upload-time = "2026-04-13T02:45:32.087Z" }, + { url = "https://files.pythonhosted.org/packages/be/33/81b64991b0f3f278c3b55c335888794af190b2d59031a5ad1401bcb69f1e/mypy-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3dc20f8ec76eecd77148cdd2f1542ed496e51e185713bf488a414f862deb8f2", size = 10889659, upload-time = "2026-04-13T02:46:02.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fd/7adcb8053572edf5ef8f3db59599dfeeee3be9cc4c8c97e2d28f66f42ac5/mypy-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:a9d62bbac5d6d46718e2b0330b25e6264463ed832722b8f7d4440ff1be3ca895", size = 9815515, upload-time = "2026-04-13T02:46:32.103Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/db831e84c81d57d4886d99feee14e372f64bbec6a9cb1a88a19e243f2ef5/mypy-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12927b9c0ed794daedcf1dab055b6c613d9d5659ac511e8d936d96f19c087d12", size = 14483064, upload-time = "2026-04-13T02:45:26.901Z" }, + { url = "https://files.pythonhosted.org/packages/d5/82/74e62e7097fa67da328ac8ece8de09133448c04d20ddeaeba251a3000f01/mypy-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:752507dd481e958b2c08fc966d3806c962af5a9433b5bf8f3bdd7175c20e34fe", size = 13335694, upload-time = "2026-04-13T02:46:12.514Z" }, + { url = "https://files.pythonhosted.org/packages/74/c4/97e9a0abe4f3cdbbf4d079cb87a03b786efeccf5bf2b89fe4f96939ab2e6/mypy-1.20.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c614655b5a065e56274c6cbbe405f7cf7e96c0654db7ba39bc680238837f7b08", size = 13726365, upload-time = "2026-04-13T02:45:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/d7/aa/a19d884a8d28fcd3c065776323029f204dbc774e70ec9c85eba228b680de/mypy-1.20.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c3f6221a76f34d5100c6d35b3ef6b947054123c3f8d6938a4ba00b1308aa572", size = 14693472, upload-time = "2026-04-13T02:46:41.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/cc9324bd21cf786592b44bf3b5d224b3923c1230ec9898d508d00241d465/mypy-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bdfc06303ac06500af71ea0cdbe995c502b3c9ba32f3f8313523c137a25d1b6", size = 14919266, upload-time = "2026-04-13T02:46:28.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dc/779abb25a8c63e8f44bf5a336217fa92790fa17e0c40e0c725d10cb01bbd/mypy-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:0131edd7eba289973d1ba1003d1a37c426b85cdef76650cd02da6420898a5eb3", size = 11049713, upload-time = "2026-04-13T02:45:57.673Z" }, + { url = "https://files.pythonhosted.org/packages/28/08/4172be2ad7de9119b5a92ca36abbf641afdc5cb1ef4ae0c3a8182f29674f/mypy-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:33f02904feb2c07e1fdf7909026206396c9deeb9e6f34d466b4cfedb0aadbbe4", size = 9999819, upload-time = "2026-04-13T02:46:35.039Z" }, + { url = "https://files.pythonhosted.org/packages/2d/af/af9e46b0c8eabbce9fc04a477564170f47a1c22b308822282a59b7ff315f/mypy-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:168472149dd8cc505c98cefd21ad77e4257ed6022cd5ed2fe2999bed56977a5a", size = 15547508, upload-time = "2026-04-13T02:46:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cd/39c9e4ad6ba33e069e5837d772a9e6c304b4a5452a14a975d52b36444650/mypy-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb674600309a8f22790cca883a97c90299f948183ebb210fbef6bcee07cb1986", size = 14399557, upload-time = "2026-04-13T02:46:10.021Z" }, + { url = "https://files.pythonhosted.org/packages/83/c1/3fd71bdc118ffc502bf57559c909927bb7e011f327f7bb8e0488e98a5870/mypy-1.20.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2b2e4cc464ba9795459f2586923abd58a0055487cbe558cb538ea6e6bc142a", size = 15045789, upload-time = "2026-04-13T02:45:10.81Z" }, + { url = "https://files.pythonhosted.org/packages/8e/73/6f07ff8b57a7d7b3e6e5bf34685d17632382395c8bb53364ec331661f83e/mypy-1.20.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee461d396dd46b3f0ed5a098dbc9b8860c81c46ad44fa071afcfbc149f167c9", size = 15850795, upload-time = "2026-04-13T02:45:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e2/f7dffec1c7767078f9e9adf0c786d1fe0ff30964a77eb213c09b8b58cb76/mypy-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e364926308b3e66f1361f81a566fc1b2f8cd47fc8525e8136d4058a65a4b4f02", size = 16088539, upload-time = "2026-04-13T02:46:17.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/76/e0dee71035316e75a69d73aec2f03c39c21c967b97e277fd0ef8fd6aec66/mypy-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a0c17fbd746d38c70cbc42647cfd884f845a9708a4b160a8b4f7e70d41f4d7fa", size = 12575567, upload-time = "2026-04-13T02:45:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/7ed43c9d9c3d1468f86605e323a5d97e411a448790a00f07e779f3211a46/mypy-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:db2cb89654626a912efda69c0d5c1d22d948265e2069010d3dde3abf751c7d08", size = 10378823, upload-time = "2026-04-13T02:45:13.35Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/926bd972388e65a39ee98e188ccf67e81beb3aacfd5d6b310051772d974b/mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06", size = 2636553, upload-time = "2026-04-13T02:46:30.45Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "openai" +version = "2.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/59/bdcc6b759b8c42dd73afaf5bf8f902c04b37987a5514dbc1c64dba390fef/openai-2.32.0.tar.gz", hash = "sha256:c54b27a9e4cb8d51f0dd94972ffd1a04437efeb259a9e60d8922b8bd26fe55e0", size = 693286, upload-time = "2026-04-15T22:28:19.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/c1/d6e64ccd0536bf616556f0cad2b6d94a8125f508d25cfd814b1d2db4e2f1/openai-2.32.0-py3-none-any.whl", hash = "sha256:4dcc9badeb4bf54ad0d187453742f290226d30150890b7890711bda4f32f192f", size = 1162570, upload-time = "2026-04-15T22:28:17.714Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/8e/3778a7e87801d994869a9396b9fc2a289e5f9be91ff54a27d41eace494b0/opentelemetry_api-1.41.0.tar.gz", hash = "sha256:9421d911326ec12dee8bc933f7839090cad7a3f13fcfb0f9e82f8174dc003c09", size = 71416, upload-time = "2026-04-09T14:38:34.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/ee/99ab786653b3bda9c37ade7e24a7b607a1b1f696063172768417539d876d/opentelemetry_api-1.41.0-py3-none-any.whl", hash = "sha256:0e77c806e6a89c9e4f8d372034622f3e1418a11bdbe1c80a50b3d3397ad0fa4f", size = 69007, upload-time = "2026-04-09T14:38:11.833Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.62b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/fd/b8e90bb340957f059084376f94cff336b0e871a42feba7d3f7342365e987/opentelemetry_instrumentation-0.62b0.tar.gz", hash = "sha256:aa1b0b9ab2e1722c2a8a5384fb016fc28d30bba51826676c8036074790d2861e", size = 34042, upload-time = "2026-04-09T14:40:22.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b6/3356d2e335e3c449c5183e9b023f30f04f1b7073a6583c68745ea2e704b1/opentelemetry_instrumentation-0.62b0-py3-none-any.whl", hash = "sha256:30d4e76486eae64fb095264a70c2c809c4bed17b73373e53091470661f7d477c", size = 34158, upload-time = "2026-04-09T14:39:21.428Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/0e/a586df1186f9f56b5a0879d52653effc40357b8e88fc50fe300038c3c08b/opentelemetry_sdk-1.41.0.tar.gz", hash = "sha256:7bddf3961131b318fc2d158947971a8e37e38b1cd23470cfb72b624e7cc108bd", size = 230181, upload-time = "2026-04-09T14:38:47.225Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/13/a7825118208cb32e6a4edcd0a99f925cbef81e77b3b0aedfd9125583c543/opentelemetry_sdk-1.41.0-py3-none-any.whl", hash = "sha256:a596f5687964a3e0d7f8edfdcf5b79cbca9c93c7025ebf5fb00f398a9443b0bd", size = 180214, upload-time = "2026-04-09T14:38:30.657Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.62b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/b0/c14f723e86c049b7bf8ff431160d982519b97a7be2857ed2247377397a24/opentelemetry_semantic_conventions-0.62b0.tar.gz", hash = "sha256:cbfb3c8fc259575cf68a6e1b94083cc35adc4a6b06e8cf431efa0d62606c0097", size = 145753, upload-time = "2026-04-09T14:38:48.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/6c/5e86fa1759a525ef91c2d8b79d668574760ff3f900d114297765eb8786cb/opentelemetry_semantic_conventions-0.62b0-py3-none-any.whl", hash = "sha256:0ddac1ce59eaf1a827d9987ab60d9315fb27aea23304144242d1fcad9e16b489", size = 231619, upload-time = "2026-04-09T14:38:32.394Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" }, + { url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" }, + { url = "https://files.pythonhosted.org/packages/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3c/b9cde05bdc7b2385c66014e0620627da638d3d04e4954416ab48c31196c5/orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f", size = 135481, upload-time = "2026-03-31T16:14:55.901Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" }, + { url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" }, + { url = "https://files.pythonhosted.org/packages/70/07/c17dcf05dd8045457538428a983bf1f1127928df5bf328cb24d2b7cddacb/orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054", size = 147729, upload-time = "2026-03-31T16:15:05.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/4d3cc3a3d616035beb51b24a09bb872942dc452cf2df0c1d11ab35046d9f/orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac", size = 131870, upload-time = "2026-03-31T16:15:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/13/26/9fe70f81d16b702f8c3a775e8731b50ad91d22dacd14c7599b60a0941cd1/orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06", size = 127440, upload-time = "2026-03-31T16:15:09.994Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c6/b038339f4145efd2859c1ca53097a52c0bb9cbdd24f947ebe146da1ad067/orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd", size = 127399, upload-time = "2026-03-31T16:15:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, + { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, + { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, + { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, + { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, + { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, + { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, + { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-httpx" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/42/f53c58570e80d503ade9dd42ce57f2915d14bcbe25f6308138143950d1d6/pytest_httpx-0.36.2.tar.gz", hash = "sha256:05a56527484f7f4e8c856419ea379b8dc359c36801c4992fdb330f294c690356", size = 57683, upload-time = "2026-04-09T13:57:19.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/55/1fa65f8e4fceb19dd6daa867c162ad845d547f6058cd92b4b02384a44777/pytest_httpx-0.36.2-py3-none-any.whl", hash = "sha256:d42ebd5679442dc7bfb0c48e0767b6562e9bc4534d805127b0084171886a5e22", size = 20315, upload-time = "2026-04-09T13:57:18.587Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + +[[package]] +name = "rust-just" +version = "1.50.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ee/4507865278fdfcb982f16cc91fd06d4be8e04048674040ffc3481c95c6ff/rust_just-1.50.0.tar.gz", hash = "sha256:3fab1409ed8662f17b13a61dc94bcd82e1e04dd6ca50e8d90fdf7ba7ce2d4c58", size = 1914349, upload-time = "2026-04-20T03:59:29.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/1d/3a962f900aba20f5ab795cb0603b021b3b863ad93231eb76f36d10b91b16/rust_just-1.50.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bc58e3849fa20ecfc0dd1811ff998eedee98fe0d8dfd66c01dcda4f1e79f7e94", size = 1986190, upload-time = "2026-04-20T05:32:40.672Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/4e7e77b7b73e5a32188f24501c3f047c848f2f437e64900fa2a11f9f91ec/rust_just-1.50.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f484f422c9eff04e7e3a25fa67c0fbf7c9d6a303ede529a85f428c076824f55d", size = 1838810, upload-time = "2026-04-20T03:59:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/80b7b44678c408a3081b4583ac5be1e229cab87631a3ea54f19e288643a8/rust_just-1.50.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6c473f74259d3c615cb447696912580e7a6b25024f38c39bb7a9fd0169c794", size = 1922138, upload-time = "2026-04-20T03:59:16.805Z" }, + { url = "https://files.pythonhosted.org/packages/05/ca/421f4751d528a7537d5c259f05178bb3bc1a4543d4747e748766aaffa16f/rust_just-1.50.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51770f537fb326a16deb9d5f0ea65551b7ce5e7dc19cf026e06b69af0d334e39", size = 1907446, upload-time = "2026-04-20T05:32:42.661Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/72bfb89bd5a577aca04f650cd5cfba4a68d9ab1952a0c2bf6736938780d9/rust_just-1.50.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1c8feb30ccbe07c2300585aaf7cf25212cec2060839d2e20352fa20c1df5d3", size = 2093897, upload-time = "2026-04-20T03:59:36.837Z" }, + { url = "https://files.pythonhosted.org/packages/25/96/f411d20054579d08d461170cea966bc3864e5136200375e2cec9f1bca7f1/rust_just-1.50.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70143fdd59e1050f71d4a8626e705e9661e25f9b55a717680f54613fe03fcbc0", size = 2165169, upload-time = "2026-04-20T03:59:21.817Z" }, + { url = "https://files.pythonhosted.org/packages/42/bf/4ae3ead0da01977971ac00d50f80a9e0963feb320d01b6a55e204eac622f/rust_just-1.50.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d55c2c9f15b3646371131f451ce506606ccf1b26ccb96bcff40ea2b2366320ba", size = 2104423, upload-time = "2026-04-20T03:59:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/f342678284cab097bd52e3f2e4bbacfd224325b353d932b5c0037774c8e5/rust_just-1.50.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f0949c25a299be2c56dc435fcb7abd1941475b0378a6c742fd6af2123395834", size = 2078252, upload-time = "2026-04-20T03:59:25.114Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6a/4ba4d5300f1546fde9534bfe85cbcb604e3df98d1ef62a4f3131d8771459/rust_just-1.50.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:361fe9c81762e25d88fa9cc86ea635252a935808c75d3b6fa6ac47f3a409a78b", size = 1938954, upload-time = "2026-04-20T03:59:18.376Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6e/d2bd6eb9413032233b5b2ba282ed31adb1e96a30f99514b7eda92b2f12d6/rust_just-1.50.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7aeebd570c8cb59891f53e2f99556bc3464bc504d635ec4ebf3ea19c8dee129", size = 1946176, upload-time = "2026-04-20T03:59:26.947Z" }, + { url = "https://files.pythonhosted.org/packages/9e/90/2de37bb61ce255f333bb68ca0e7810c3f8ffd2df0d0ab5d6b05b6d6b6e4d/rust_just-1.50.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4e828bcecf360be0945866d4815ecebcee20bb59657c1e02238089ad1bbcb2b8", size = 1932433, upload-time = "2026-04-20T03:59:34.002Z" }, + { url = "https://files.pythonhosted.org/packages/25/e6/6f13656e03146914cd2a6ff64ccf0b13edc6aa891ddc09d4e7d15d5f8dea/rust_just-1.50.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6fdadbd55cf41a7d7a470354c4ad3767b2376e205f30229b31ced12bfcf52854", size = 2064863, upload-time = "2026-04-20T03:59:19.975Z" }, + { url = "https://files.pythonhosted.org/packages/1c/01/a71cccef636db4cb91bb33fef3f919dc44378373c9b7881c820d53faeded/rust_just-1.50.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:520fbf76a5a54d857d1c8357d7a68c36764c56901efadbf99d878f614f60e956", size = 2109308, upload-time = "2026-04-20T03:59:14.858Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5f/6e2a45166ba01bc7f463ca348bbc4d7032bb3b079dcc7ba5938e98ffc286/rust_just-1.50.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c78de8afa60da90487b37d9991aea18a8dd5e09cafc7ac196792c5998f1e74c", size = 2151063, upload-time = "2026-04-20T05:32:45.614Z" }, + { url = "https://files.pythonhosted.org/packages/98/c4/be6a38523db6761c030bcede7a0c16216d86eb7eb34a00abdf87cb3ba811/rust_just-1.50.0-py3-none-win32.whl", hash = "sha256:4f661445d94be7c54c4333a0956a7e7b31c805a1c5c4966d04e06b694cf6b3d1", size = 1853369, upload-time = "2026-04-20T03:59:35.456Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2f/75b8008081894e6cd90eb47baf6088a5d61d92b98000fddb408179588f64/rust_just-1.50.0-py3-none-win_amd64.whl", hash = "sha256:2995c8e8feca32d2791010dabbef699a49d0f165f535df4d97fb0f520f2924bb", size = 2060932, upload-time = "2026-04-20T03:59:28.359Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "stevedore" +version = "5.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uipath-core" +version = "0.5.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/f2/9b6d5eb0a4e5b2a4a80c777bc4b7a22efb8e343ef48766ed3ab73d6b11eb/uipath_core-0.5.11.tar.gz", hash = "sha256:9ed987360e7439f53b07e4d10d2381cacc80443f43f3fcf7f721d46ac3320c95", size = 117024, upload-time = "2026-04-06T14:31:38.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/bd/f28d89dcaec4ea040efb17ad9f8229675f293cff5f0a310351003d7f92b7/uipath_core-0.5.11-py3-none-any.whl", hash = "sha256:8c107dd9597d20c4ca7d8e770e703d36b0e21a8a1d80e13d585f125eb64d54bf", size = 43283, upload-time = "2026-04-06T14:31:36.908Z" }, +] + +[[package]] +name = "uipath-eval" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "opentelemetry-sdk" }, + { name = "pydantic" }, + { name = "uipath-core" }, +] + +[package.optional-dependencies] +llm = [ + { name = "langchain-core" }, + { name = "openai" }, +] + +[package.dev-dependencies] +dev = [ + { name = "bandit" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-httpx" }, + { name = "pytest-mock" }, + { name = "ruff" }, + { name = "rust-just" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "langchain-core", marker = "extra == 'llm'", specifier = ">=0.3" }, + { name = "openai", marker = "extra == 'llm'", specifier = ">=1.0" }, + { name = "opentelemetry-sdk", specifier = ">=1.39.0,<2.0.0" }, + { name = "pydantic", specifier = ">=2.12.5,<3.0.0" }, + { name = "uipath-core", specifier = ">=0.5.8,<0.6.0" }, +] +provides-extras = ["llm"] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit", specifier = ">=1.8.2" }, + { name = "mypy", specifier = ">=1.14.1" }, + { name = "pre-commit", specifier = ">=4.5.1" }, + { name = "pytest", specifier = ">=7.4.0" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "pytest-cov", specifier = ">=4.1.0" }, + { name = "pytest-httpx", specifier = ">=0.35.0" }, + { name = "pytest-mock", specifier = ">=3.11.1" }, + { name = "ruff", specifier = ">=0.9.4" }, + { name = "rust-just", specifier = ">=1.39.0" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, + { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, + { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, + { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, + { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/6c64bdbf71f58ccde7919e00491812556f446a5291573af92c49a5e9aaef/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8", size = 591617, upload-time = "2026-02-20T22:50:24.532Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f0/758c3b0fb0c4871c7704fef26a5bc861de4f8a68e4831669883bebe07b0f/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c", size = 303702, upload-time = "2026-02-20T22:50:40.687Z" }, + { url = "https://files.pythonhosted.org/packages/85/89/d91862b544c695cd58855efe3201f83894ed82fffe34500774238ab8eba7/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf", size = 337678, upload-time = "2026-02-20T22:50:39.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6b/cf342ba8a898f1de024be0243fac67c025cad530c79ea7f89c4ce718891a/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533", size = 343711, upload-time = "2026-02-20T22:50:43.965Z" }, + { url = "https://files.pythonhosted.org/packages/b3/20/049418d094d396dfa6606b30af925cc68a6670c3b9103b23e6990f84b589/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62", size = 476731, upload-time = "2026-02-20T22:50:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/0857f64d53a90321e6a46a3d4cc394f50e1366132dcd2ae147f9326ca98b/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01", size = 338902, upload-time = "2026-02-20T22:50:33.927Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d0/5bf7cbf1ac138c92b9ac21066d18faf4d7e7f651047b700eb192ca4b9fdb/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2", size = 364700, upload-time = "2026-02-20T22:50:21.732Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, +] + +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, + { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" }, + { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +] diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index a8d04271e..36811af71 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,11 +1,12 @@ [project] name = "uipath" -version = "2.10.54" +version = "2.10.55" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.8, <0.6.0", + "uipath-eval>=0.1.0, <0.2.0", "uipath-runtime>=0.10.1, <0.11.0", "uipath-platform>=0.1.13, <0.2.0", "click>=8.3.1", @@ -149,6 +150,7 @@ source = ["src"] [tool.uv.sources] uipath-core = { path = "../uipath-core", editable = true } +uipath-eval = { path = "../uipath-eval", editable = true } uipath-platform = { path = "../uipath-platform", editable = true } [[tool.uv.index]] diff --git a/packages/uipath/src/uipath/eval/__init__.py b/packages/uipath/src/uipath/eval/__init__.py new file mode 100644 index 000000000..05513d1d8 --- /dev/null +++ b/packages/uipath/src/uipath/eval/__init__.py @@ -0,0 +1,79 @@ +"""UiPath eval module — platform-independent evaluators via uipath-eval, runtime integration in uipath.""" + +from uipath_eval import ( + EVALUATORS, + AgentExecution, + BaseEvaluationCriteria, + BaseEvaluator, + BaseEvaluatorConfig, + BaseEvaluatorJustification, + BaseLegacyEvaluator, + BinaryClassificationEvaluator, + BooleanEvaluationResult, + ContainsEvaluator, + ErrorEvaluationResult, + EvalItemResult, + EvaluationResult, + EvaluationResultDto, + EvaluatorType, + ExactMatchEvaluator, + JsonSimilarityEvaluator, + LegacyEvaluatorCategory, + LegacyEvaluatorType, + LLMResponse, + MulticlassClassificationEvaluator, + NumericEvaluationResult, + ScoreType, + ToolCall, + ToolCallArgsEvaluator, + ToolCallCountEvaluator, + ToolCallOrderEvaluator, + ToolCallOutputEvaluator, + ToolOutput, +) + +from uipath.eval.evaluators.legacy_exact_match_evaluator import ( + LegacyExactMatchEvaluator, +) +from uipath.eval.evaluators.legacy_json_similarity_evaluator import ( + LegacyJsonSimilarityEvaluator, +) + +__all__ = [ + "EVALUATORS", + # Base classes + "BaseEvaluator", + "BaseEvaluationCriteria", + "BaseEvaluatorConfig", + "BaseEvaluatorJustification", + "BaseLegacyEvaluator", + # Coded evaluators + "BinaryClassificationEvaluator", + "ContainsEvaluator", + "ExactMatchEvaluator", + "JsonSimilarityEvaluator", + "MulticlassClassificationEvaluator", + # Tool call evaluators + "ToolCallArgsEvaluator", + "ToolCallCountEvaluator", + "ToolCallOrderEvaluator", + "ToolCallOutputEvaluator", + # Legacy deterministic evaluators + "LegacyExactMatchEvaluator", + "LegacyJsonSimilarityEvaluator", + # Models + "AgentExecution", + "BooleanEvaluationResult", + "ErrorEvaluationResult", + "EvalItemResult", + "EvaluationResult", + "EvaluationResultDto", + "EvaluatorType", + "LegacyEvaluatorCategory", + "LegacyEvaluatorType", + "LLMResponse", + "NumericEvaluationResult", + "ScoreType", + "ToolCall", + "ToolOutput", +] diff --git a/packages/uipath/src/uipath/eval/evaluators/__init__.py b/packages/uipath/src/uipath/eval/evaluators/__init__.py index 03a4bf63b..7b4802c57 100644 --- a/packages/uipath/src/uipath/eval/evaluators/__init__.py +++ b/packages/uipath/src/uipath/eval/evaluators/__init__.py @@ -2,25 +2,35 @@ from typing import Any -# Current coded evaluators -from .base_evaluator import ( +# Platform-independent evaluators sourced from uipath-eval +from uipath_eval.evaluators import EVALUATORS as _EVAL_EVALUATORS +from uipath_eval.evaluators import ( BaseEvaluationCriteria, BaseEvaluator, BaseEvaluatorConfig, BaseEvaluatorJustification, + BinaryClassificationEvaluator, + ContainsEvaluator, + JsonSimilarityEvaluator, + LegacyJsonSimilarityEvaluator, + MulticlassClassificationEvaluator, + ToolCallArgsEvaluator, + ToolCallCountEvaluator, + ToolCallOrderEvaluator, + ToolCallOutputEvaluator, ) + +# Platform-extended BaseLegacyEvaluator — extends uipath_eval's with line-by-line + attachments from .base_legacy_evaluator import BaseLegacyEvaluator -from .binary_classification_evaluator import BinaryClassificationEvaluator -# Legacy evaluators -from .contains_evaluator import ContainsEvaluator +# Platform-extended ExactMatchEvaluator — uses local OutputEvaluator with attachment support from .exact_match_evaluator import ExactMatchEvaluator -from .json_similarity_evaluator import JsonSimilarityEvaluator + +# Platform-dependent evaluators (LLM, langchain, uipath-platform) — stay in uipath from .legacy_context_precision_evaluator import LegacyContextPrecisionEvaluator from .legacy_csv_exact_match_evaluator import LegacyCSVExactMatchEvaluator from .legacy_exact_match_evaluator import LegacyExactMatchEvaluator from .legacy_faithfulness_evaluator import LegacyFaithfulnessEvaluator -from .legacy_json_similarity_evaluator import LegacyJsonSimilarityEvaluator from .legacy_llm_as_judge_evaluator import LegacyLlmAsAJudgeEvaluator from .legacy_trajectory_evaluator import LegacyTrajectoryEvaluator from .llm_as_judge_evaluator import LLMJudgeJustification @@ -34,27 +44,20 @@ LLMJudgeTrajectoryEvaluator, LLMJudgeTrajectorySimulationEvaluator, ) -from .multiclass_classification_evaluator import MulticlassClassificationEvaluator from .output_evaluator import AggregationMethod -from .tool_call_args_evaluator import ToolCallArgsEvaluator -from .tool_call_count_evaluator import ToolCallCountEvaluator -from .tool_call_order_evaluator import ToolCallOrderEvaluator -from .tool_call_output_evaluator import ToolCallOutputEvaluator EVALUATORS: list[type[BaseEvaluator[Any, Any, Any]]] = [ + # Replace uipath_eval.ExactMatchEvaluator with local version (has attachment support) + *( + e + for e in _EVAL_EVALUATORS + if e.get_evaluator_id() != ExactMatchEvaluator.get_evaluator_id() + ), ExactMatchEvaluator, - ContainsEvaluator, - BinaryClassificationEvaluator, - MulticlassClassificationEvaluator, - JsonSimilarityEvaluator, LLMJudgeOutputEvaluator, LLMJudgeStrictJSONSimilarityOutputEvaluator, LLMJudgeTrajectoryEvaluator, LLMJudgeTrajectorySimulationEvaluator, - ToolCallOrderEvaluator, - ToolCallArgsEvaluator, - ToolCallCountEvaluator, - ToolCallOutputEvaluator, ] __all__ = [ # Legacy evaluators diff --git a/packages/uipath/src/uipath/eval/evaluators/base_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/base_evaluator.py index 73fac46c6..1eb8ad11e 100644 --- a/packages/uipath/src/uipath/eval/evaluators/base_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/base_evaluator.py @@ -1,642 +1,17 @@ -"""Base evaluator abstract class for agent evaluation.""" - -import json -from abc import ABC, abstractmethod -from typing import Any, Generic, TypeVar, Union, cast, get_args - -from pydantic import BaseModel, ConfigDict, Field, model_validator -from pydantic.alias_generators import to_camel - -from .._helpers.helpers import track_evaluation_metrics -from ..models import AgentExecution, EvaluationResult -from ..models.models import ( - EvaluationResultDto, - UiPathEvaluationError, - UiPathEvaluationErrorCategory, +"""Re-exports from uipath-eval — single canonical BaseEvaluator hierarchy.""" + +from uipath_eval.evaluators.base_evaluator import ( + BaseEvaluationCriteria, + BaseEvaluator, + BaseEvaluatorConfig, + BaseEvaluatorJustification, + GenericBaseEvaluator, ) - -class BaseEvaluationCriteria(BaseModel): - """Base class for all evaluation criteria.""" - - model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) - pass - - -# Type variable for evaluation criteria, used by both Config and Evaluator -T = TypeVar("T", bound=BaseEvaluationCriteria) - - -class BaseEvaluatorConfig(BaseModel, Generic[T]): - """Base class for all evaluator configurations. - - Generic over T (evaluation criteria type) to ensure type safety between - the config's default_evaluation_criteria and the evaluator's expected criteria type. - """ - - model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) - - name: str = Field(description="The name of the evaluator") - description: str = Field(default="", description="The description of the evaluator") - default_evaluation_criteria: T | None = None - - -class BaseEvaluatorJustification(BaseModel): - """Base class for all evaluator justifications.""" - - expected: str - actual: str - - -# Additional type variables for Config and Justification -# Note: C must be BaseEvaluatorConfig[T] to ensure type consistency -C = TypeVar("C", bound=BaseEvaluatorConfig[Any]) -J = TypeVar("J", bound=Union[str, BaseEvaluatorJustification]) - - -class GenericBaseEvaluator(BaseModel, Generic[T, C, J], ABC): - """Abstract base class for all evaluators. - - Generic Parameters: - T: The evaluation criteria type (bound to BaseEvaluationCriteria) - C: The evaluator config type (bound to BaseEvaluatorConfig[T]) - J: The justification type (str, None, or BaseEvaluatorJustification subclass) - - Design Rationale: - T is explicitly specified even though C = BaseEvaluatorConfig[T] already encodes it. - This redundancy is intentional and provides: - - 1. **Type Checker Support**: Static type checkers can infer the exact criteria type - for the evaluate() method signature without runtime introspection - - 2. **Clear API**: The signature BaseEvaluator[MyCriteria, MyConfig[MyCriteria], str] - makes it immediately obvious what criteria type is expected - - 3. **IDE Support**: Autocomplete and type hints work perfectly for method parameters - - Runtime validation ensures T and C's generic parameter are consistent. - """ - - model_config = ConfigDict(arbitrary_types_allowed=True) - - id: str - name: str = Field(default="", description="The name of the evaluator") - description: str = Field(default="", description="The description of the evaluator") - - config_type: type[C] = Field(description="The config type class", exclude=True) - evaluation_criteria_type: type[T] = Field( - description="The type used for evaluation criteria validation and creation", - exclude=True, - ) - justification_type: type[J] = Field( - description="The type used for justification validation and creation", - exclude=True, - ) - - def __init_subclass__(cls, **kwargs: Any): - """Hook for subclass creation - automatically applies evaluation metrics tracking.""" - super().__init_subclass__(**kwargs) - - if hasattr(cls, "evaluate") and not getattr( - cls.evaluate, "_has_metrics_decorator", False - ): - new_evaluation_method = track_evaluation_metrics(cls.evaluate) - new_evaluation_method._has_metrics_decorator = True # type: ignore[attr-defined] # probably a better way to do this - cls.evaluate = new_evaluation_method # type: ignore[method-assign] # probably a better way to do this - - @model_validator(mode="before") - @classmethod - def validate_model(cls, values: Any) -> Any: - """Pre-initialization model validator for Pydantic models. - - This validator extracts the Generic type parameters and validates their consistency. - - Args: - values: The raw input values before validation - - Returns: - The validated/transformed values with types set - - Raises: - ValueError: If types cannot be determined or are inconsistent - """ - if isinstance(values, dict): - if "description" in values and "evaluatorConfig" in values: - values["evaluatorConfig"]["description"] = values.pop("description") - if "name" in values and "evaluatorConfig" in values: - values["evaluatorConfig"]["name"] = values.pop("name") - # Always extract and set evaluation_criteria_type - criteria_type = cls._extract_evaluation_criteria_type() - values["evaluation_criteria_type"] = criteria_type - - # Always extract and set config_type - config_type = cls._extract_config_type() - values["config_type"] = config_type - - # Always extract and set justification_type - justification_type = cls._extract_justification_type() - values["justification_type"] = justification_type - - # Validate consistency: config's generic parameter should match criteria_type - cls._validate_type_consistency(config_type, criteria_type) - - # Validate and create the config object if config dict is provided - try: - raw_config = values.get("config") or values.get("evaluatorConfig") or {} - validated_config = config_type.model_validate(raw_config) - values["evaluator_config"] = validated_config - except Exception as e: - raise UiPathEvaluationError( - code="FAILED_TO_VALIDATE_EVALUATOR_CONFIG", - title=f"Failed to validate evaluator config for {cls.__name__}", - detail=f"Error: {e}", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) from e - - return values - - @classmethod - def _validate_type_consistency( - cls, - config_type: type[BaseEvaluatorConfig[Any]], - criteria_type: type[BaseEvaluationCriteria], - ) -> None: - """Validate that the config's generic parameter matches the evaluator's criteria type. - - Extracts the criteria type from the config's default_evaluation_criteria field - annotation and validates it matches the evaluator's expected criteria type. - - Args: - config_type: The config type to validate - criteria_type: The expected evaluation criteria type - - Raises: - ValueError: If the types are inconsistent - """ - # Skip validation for base classes - if config_type.__name__ in ( - "BaseEvaluatorConfig", - "OutputEvaluatorConfig", - "BaseLLMJudgeEvaluatorConfig", - ): - return - - # Extract from Pydantic's model_fields which preserves generic types - if ( - hasattr(config_type, "model_fields") - and "default_evaluation_criteria" in config_type.model_fields - ): - field_info = config_type.model_fields["default_evaluation_criteria"] - if hasattr(field_info, "annotation"): - annotation = field_info.annotation - # The annotation will be SomeCriteria | None - args = get_args(annotation) - if args: - # Get the criteria type (the non-None arg) - for arg in args: - if ( - arg is not type(None) - and isinstance(arg, type) - and issubclass(arg, BaseEvaluationCriteria) - ): - # Found the config's criteria type, check if it matches - if arg != criteria_type: - raise UiPathEvaluationError( - code="TYPE_INCONSISTENCY_IN_EVALUATOR", - title=f"Type inconsistency in {cls.__name__}: " - f"Config {config_type.__name__} expects criteria type {arg.__name__}", - detail=f"Evaluator expects {criteria_type.__name__}. " - f"Ensure BaseEvaluator[T, C[T], J] has matching T and C[T] parameters.", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - return # Validation passed - - @classmethod - def _extract_evaluation_criteria_type(cls) -> type[BaseEvaluationCriteria]: - """Extract the evaluation criteria type from Pydantic model fields. - - Returns: - The evaluation criteria type - - Raises: - ValueError: If no valid evaluation criteria type can be determined from the class definition - """ - # Special case: if this is the BaseEvaluator class itself, return BaseEvaluationCriteria - if cls.__name__ == ("BaseEvaluator" or "BaseEvaluator[Any, Any, Any]"): - return BaseEvaluationCriteria - - # Check if Pydantic has already resolved the evaluation_criteria_type field annotation - if not ( - hasattr(cls, "model_fields") - and "evaluation_criteria_type" in cls.model_fields - ): - raise UiPathEvaluationError( - code="COULD_NOT_FIND_EVALUATION_CRITERIA_TYPE_FIELD", - title=f"Could not find evaluation_criteria_type field in {cls.__name__}", - detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - field_info = cls.model_fields["evaluation_criteria_type"] - if not hasattr(field_info, "annotation"): - raise UiPathEvaluationError( - code="NO_ANNOTATION_FOUND_FOR_EVALUATION_CRITERIA_TYPE_FIELD", - title=f"No annotation found for evaluation_criteria_type field in {cls.__name__}", - detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - # Extract the inner type from type[SomeType] - annotation = field_info.annotation - args = get_args(annotation) - if not args: - raise UiPathEvaluationError( - code="INVALID_ANNOTATION_FOR_EVALUATION_CRITERIA_TYPE", - title=f"Invalid annotation for evaluation_criteria_type in {cls.__name__}: {annotation}", - detail="Expected type[SomeEvaluationCriteria]", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - criteria_type = args[0] - if not ( - isinstance(criteria_type, type) - and issubclass(criteria_type, BaseEvaluationCriteria) - ): - raise UiPathEvaluationError( - code="INVALID_EVALUATION_CRITERIA_TYPE", - title=f"Invalid evaluation criteria type {criteria_type} in {cls.__name__}", - detail=f"{criteria_type} must be a subclass of BaseEvaluationCriteria", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - return criteria_type - - @classmethod - def _extract_config_type(cls) -> type[BaseEvaluatorConfig[Any]]: - """Extract the config type from Pydantic model fields. - - Returns: - The config type for this evaluator - - Raises: - ValueError: If no valid config type can be determined from the class definition - """ - # Special case: if this is the BaseEvaluator class itself, return BaseEvaluatorConfig - if cls.__name__ == ("BaseEvaluator" or "BaseEvaluator[Any, Any, Any]"): - return BaseEvaluatorConfig - # Check if Pydantic has already resolved the config_type field annotation - if not (hasattr(cls, "model_fields") and "config_type" in cls.model_fields): - raise UiPathEvaluationError( - code="COULD_NOT_FIND_CONFIG_TYPE_FIELD", - title=f"Could not find config_type field in {cls.__name__}", - detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - field_info = cls.model_fields["config_type"] - if not hasattr(field_info, "annotation"): - raise UiPathEvaluationError( - code="NO_ANNOTATION_FOUND_FOR_CONFIG_TYPE_FIELD", - title=f"No annotation found for config_type field in {cls.__name__}", - detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - # Extract the inner type from type[SomeType] - annotation = field_info.annotation - args = get_args(annotation) - if not args: - raise UiPathEvaluationError( - code="INVALID_ANNOTATION_FOR_CONFIG_TYPE", - title=f"Invalid annotation for config_type in {cls.__name__}: {annotation}", - detail="Expected type[SomeEvaluatorConfig]", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - config_type = args[0] - if not ( - isinstance(config_type, type) - and issubclass(config_type, BaseEvaluatorConfig) - ): - raise UiPathEvaluationError( - code="INVALID_CONFIG_TYPE", - title=f"Invalid config type {config_type} in {cls.__name__}", - detail=f"{config_type} must be a subclass of BaseEvaluatorConfig", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - return config_type - - @classmethod - def _extract_justification_type(cls) -> type[J]: - """Extract the justification type from Pydantic model fields. - - Returns: - The justification type (str or BaseEvaluatorJustification subclass) - - Raises: - UiPathEvaluationError: If no valid justification type can be determined - """ - try: - # Check if Pydantic has resolved the justification_type field annotation - if not ( - hasattr(cls, "model_fields") - and "justification_type" in cls.model_fields - ): - raise UiPathEvaluationError( - code="COULD_NOT_FIND_JUSTIFICATION_TYPE_FIELD", - title=f"Could not find justification_type field in {cls.__name__}", - detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - field_info = cls.model_fields["justification_type"] - if not hasattr(field_info, "annotation"): - raise UiPathEvaluationError( - code="NO_ANNOTATION_FOUND_FOR_JUSTIFICATION_TYPE_FIELD", - title=f"No annotation found for justification_type field in {cls.__name__}", - detail="Ensure the class properly inherits from BaseEvaluator with correct Generic parameters.", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - # Extract the inner type from type[SomeType] - annotation = field_info.annotation - args = get_args(annotation) - if not args: - raise UiPathEvaluationError( - code="INVALID_ANNOTATION_FOR_JUSTIFICATION_TYPE", - title=f"Invalid annotation for justification_type in {cls.__name__}: {annotation}", - detail="Expected type[str] or type[SomeEvaluatorJustification]", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - justification_type = args[0] - - # Validate the justification type - must be str or BaseEvaluatorJustification subclass - if justification_type is str: - return cast(type[J], justification_type) - elif isinstance(justification_type, type) and issubclass( - justification_type, BaseEvaluatorJustification - ): - return cast(type[J], justification_type) - else: - raise UiPathEvaluationError( - code="INVALID_JUSTIFICATION_TYPE", - title=f"Invalid justification type {justification_type} in {cls.__name__}", - detail="Must be str or subclass of BaseEvaluatorJustification.", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - except UiPathEvaluationError: - raise - except Exception as e: - raise UiPathEvaluationError( - code="CANNOT_EXTRACT_JUSTIFICATION_TYPE", - title=f"Cannot extract justification type from {cls.__name__}", - detail=f"Error: {e}", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) from e - - def validate_evaluation_criteria(self, criteria: Any) -> T: - """Validate and convert input to the correct evaluation criteria type. - - Uses Pydantic's model_validate for proper validation, type coercion, - and error handling. - - Args: - criteria: The criteria to validate (dict, BaseEvaluationCriteria, or other) - - Returns: - An instance of the evaluation criteria type (T) - - Raises: - ValueError: If the criteria cannot be converted to the expected type - """ - try: - if isinstance(criteria, self.evaluation_criteria_type): - return criteria - elif isinstance(criteria, dict): - return self.evaluation_criteria_type.model_validate(criteria) - elif hasattr(criteria, "__dict__"): - # Try to convert from another object type - return self.evaluation_criteria_type.model_validate(criteria.__dict__) - else: - # Try to let Pydantic handle the conversion - return self.evaluation_criteria_type.model_validate(criteria) - except Exception as e: - raise UiPathEvaluationError( - code="CANNOT_VALIDATE_EVALUATION_CRITERIA", - title=f"Cannot validate {type(criteria)} to {self.evaluation_criteria_type}", - detail=f"Error: {e}", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) from e - - def validate_justification(self, justification: Any) -> J: - """Validate and convert input to the correct justification type. - - Args: - justification: The justification to validate (str, dict, BaseEvaluatorJustification, or other) - - Returns: - The validated justification of the correct type - """ - try: - # Handle str type - when J is bound to str - if self.justification_type is str: - if justification is None: - return cast(J, "") - return cast(J, str(justification)) - - # Handle BaseEvaluatorJustification subclasses - when J is bound to a specific subclass - if isinstance(self.justification_type, type) and issubclass( - self.justification_type, BaseEvaluatorJustification - ): - if justification is None: - raise ValueError( - f"None is not allowed for justification type {self.justification_type}" - ) - - if isinstance(justification, self.justification_type): - return justification - elif isinstance(justification, dict): - return self.justification_type.model_validate(justification) - elif hasattr(justification, "__dict__"): - return self.justification_type.model_validate( - justification.__dict__ - ) - else: - return self.justification_type.model_validate(justification) - except Exception as e: - raise UiPathEvaluationError( - code="CANNOT_CONVERT_JUSTIFICATION", - title=f"Cannot convert {type(justification)} to {self.justification_type}", - detail=f"Error: {e}", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) from e - - # Fallback: this should never happen - raise UiPathEvaluationError( - code="UNSUPPORTED_JUSTIFICATION_TYPE", - title=f"Unsupported justification type {self.justification_type} for input {type(justification)}", - detail=f"Unsupported justification type {self.justification_type} for input {type(justification)}", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - @classmethod - def get_evaluation_criteria_schema(cls) -> dict[str, Any]: - """Get the JSON schema for the evaluation criteria type. - - Returns: - The JSON schema for the evaluation criteria type - """ - criteria_type = cls._extract_evaluation_criteria_type() - return criteria_type.model_json_schema(by_alias=False) - - @classmethod - def get_config_schema(cls) -> dict[str, Any]: - """Get the JSON schema for the config type. - - Returns: - The JSON schema for the config type - """ - config_type = cls._extract_config_type() - return config_type.model_json_schema(by_alias=False) - - @classmethod - def get_justification_schema(cls) -> dict[str, Any]: - """Get the JSON schema for the justification type. - - Returns: - The JSON schema for the justification type - """ - justification_type = cls._extract_justification_type() - if justification_type is str: - return {"type": "string"} - elif isinstance(justification_type, type) and issubclass( - justification_type, BaseEvaluatorJustification - ): - return justification_type.model_json_schema(by_alias=False) - else: - raise UiPathEvaluationError( - code="INVALID_JUSTIFICATION_TYPE", - title=f"Invalid justification type {justification_type} in {cls.__name__}", - detail="Must be str or subclass of BaseEvaluatorJustification", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - - def _canonical_json(self, obj: Any) -> str: - """Convert an object to canonical JSON string for consistent comparison. - - Args: - obj: The object to convert to canonical JSON - - Returns: - str: Canonical JSON string with normalized numbers and sorted keys - """ - return json.dumps( - obj, - sort_keys=True, - separators=(",", ":"), - ensure_ascii=False, - ) - - @classmethod - @abstractmethod - def get_evaluator_id(cls) -> str: - """Get the evaluator id.""" - pass - - @classmethod - def generate_json_type(cls) -> dict[str, Any]: - """Generate the JSON schema for the evaluator.""" - return { - "evaluatorTypeId": cls.get_evaluator_id(), - "evaluatorConfigSchema": cls.get_config_schema(), - "evaluationCriteriaSchema": cls.get_evaluation_criteria_schema(), - "justificationSchema": cls.get_justification_schema(), - } - - def reduce_scores(self, results: list[EvaluationResultDto]) -> float: - """Reduce per-datapoint results into a single aggregated score. - - Default implementation computes a simple average of scores. Subclasses - can override this to implement custom aggregation logic (e.g., precision, - recall) using the rich per-datapoint data in EvaluationResultDto. - - Args: - results: List of per-datapoint results, each containing the score - and evaluation details/justification. - - Returns: - The aggregated score - """ - if not results: - return 0.0 - return sum(r.score for r in results) / len(results) - - @abstractmethod - async def validate_and_evaluate_criteria( - self, agent_execution: AgentExecution, evaluation_criteria: Any - ) -> EvaluationResult: - """Evaluate the given data and return a result from a raw evaluation criteria.""" - pass - - @abstractmethod - async def evaluate( - self, agent_execution: AgentExecution, evaluation_criteria: T - ) -> EvaluationResult: - """Evaluate the given data and return a result. - - Args: - agent_execution: The execution details containing: - - agent_input: The input received by the agent - - agent_output: The actual output from the agent - - agent_trace: The execution trace from the agent - - simulation_instructions: The simulation instructions for the agent - evaluation_criteria: The criteria to evaluate - - Returns: - EvaluationResult containing the score and details - """ - pass - - -class BaseEvaluator(GenericBaseEvaluator[T, C, J]): - """Abstract base class for all coded evaluators. Not naming this BaseCodedEvaluator for backwards compatibility.""" - - version: str = Field(default="1.0", description="Version of the evaluator") - evaluator_type_id: str = Field( - default="", alias="evaluatorTypeId", description="Type of the evaluator" - ) - evaluator_config: C = Field( - alias="evaluatorConfig", description="The validated config object instance" - ) - - name: str = Field(default="", description="The name of the evaluator", exclude=True) - description: str = Field( - default="", description="The description of the evaluator", exclude=True - ) - - def model_post_init(self, __context: Any) -> None: - """Post initialization of the evaluator.""" - if not self.evaluator_type_id: - self.evaluator_type_id = type(self).get_evaluator_id() - if not self.name: - self.name = self.evaluator_config.name - if not self.description: - self.description = self.evaluator_config.description - - async def validate_and_evaluate_criteria( - self, agent_execution: AgentExecution, evaluation_criteria: Any - ) -> EvaluationResult: - """Evaluate the given data and return a result from a raw evaluation criteria.""" - if evaluation_criteria is None: - evaluation_criteria = self.evaluator_config.default_evaluation_criteria - if evaluation_criteria is None: - raise UiPathEvaluationError( - code="NO_EVALUATION_CRITERIA_PROVIDED", - title="No evaluation criteria provided and no default evaluation criteria configured", - detail="No evaluation criteria provided and no default evaluation criteria configured", - category=UiPathEvaluationErrorCategory.SYSTEM, - ) - criteria = self.validate_evaluation_criteria(evaluation_criteria) - return await self.evaluate(agent_execution, criteria) +__all__ = [ + "BaseEvaluationCriteria", + "BaseEvaluator", + "BaseEvaluatorConfig", + "BaseEvaluatorJustification", + "GenericBaseEvaluator", +] diff --git a/packages/uipath/src/uipath/eval/evaluators/base_legacy_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/base_legacy_evaluator.py index d53dcda20..c04bc680d 100644 --- a/packages/uipath/src/uipath/eval/evaluators/base_legacy_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/base_legacy_evaluator.py @@ -1,126 +1,61 @@ -"""Base evaluator abstract class for agent evaluation.""" +"""UiPath platform extension of BaseLegacyEvaluator. + +Adds line-by-line evaluation and job attachment URI support on top of the +canonical uipath_eval.BaseLegacyEvaluator so all legacy evaluators form a +single class hierarchy. +""" -import functools -import time -from abc import ABC, abstractmethod -from collections.abc import Callable from typing import Any, Generic, TypeVar -from pydantic import ConfigDict, Field +from pydantic import Field -from ..models import EvaluationResult -from ..models.models import ( - AgentExecution, - ErrorEvaluationResult, - LegacyEvaluatorCategory, - LegacyEvaluatorType, +# Re-export so importers of this module get everything they need +from uipath_eval.evaluators.base_evaluator import ( # noqa: F401 + BaseEvaluationCriteria, + BaseEvaluatorConfig, + GenericBaseEvaluator, +) +from uipath_eval.evaluators.base_legacy_evaluator import ( + BaseLegacyEvaluator as _BaseLegacyEvaluator, ) +from uipath_eval.evaluators.base_legacy_evaluator import ( + LegacyEvaluationCriteria, + LegacyEvaluatorConfig, + track_evaluation_metrics, +) +from uipath_eval.models.models import AgentExecution + +from ..models import EvaluationResult from .attachment_utils import ( download_attachment_as_string, extract_attachment_id, is_job_attachment_uri, ) -from .base_evaluator import ( - BaseEvaluationCriteria, - BaseEvaluatorConfig, - GenericBaseEvaluator, -) from .line_by_line_utils import split_into_lines - -def track_evaluation_metrics(func: Callable[..., Any]) -> Callable[..., Any]: - """Decorator to track evaluation metrics and handle errors gracefully.""" - - @functools.wraps(func) - async def wrapper(*args: Any, **kwargs: Any) -> EvaluationResult: - start_time = time.time() - try: - result = await func(*args, **kwargs) - except Exception as e: - result = ErrorEvaluationResult( - details="Exception thrown by evaluator: {}".format(e), - evaluation_time=time.time() - start_time, - ) - end_time = time.time() - execution_time = end_time - start_time - - result.evaluation_time = execution_time - return result - - return wrapper - - -# Legacy evaluator config (non-generic version for simplicity) -class LegacyEvaluatorConfig(BaseEvaluatorConfig[BaseEvaluationCriteria]): - """Configuration for legacy evaluators.""" - - name: str = "LegacyEvaluator" - default_evaluation_criteria: None = None # Legacy evaluators don't use this - - -class LegacyEvaluationCriteria(BaseEvaluationCriteria): - """Legacy evaluation criteria.""" - - expected_output: Any = Field(alias="expectedOutput") - expected_agent_behavior: str = Field(alias="expectedAgentBehavior") - - T = TypeVar("T", bound=LegacyEvaluatorConfig) -class BaseLegacyEvaluator( - GenericBaseEvaluator[LegacyEvaluationCriteria, T, str], Generic[T], ABC -): - """Abstract base class for all legacy evaluators. +class BaseLegacyEvaluator(_BaseLegacyEvaluator[T], Generic[T]): + """UiPath-platform extension of the canonical BaseLegacyEvaluator. - Inherits from BaseEvaluator to share common evaluator infrastructure while maintaining - legacy-specific fields and behavior. + Adds: + - Line-by-line evaluation (``lineByLineEvaluation``) + - Job attachment URI auto-download in ``_get_actual_output`` """ - model_config = ConfigDict(arbitrary_types_allowed=True) - - # Required Fields - category: LegacyEvaluatorCategory = Field(...) - type: LegacyEvaluatorType = Field(...) - - # Optional Fields - file_name: str = Field(default="", alias="fileName") - target_output_key: str = Field(default="*", alias="targetOutputKey") - created_at: str = Field(..., alias="createdAt") - updated_at: str = Field(..., alias="updatedAt") - - # Line-by-line evaluation support line_by_line_evaluation: bool = Field(default=False, alias="lineByLineEvaluation") line_delimiter: str = Field(default="\n", alias="lineDelimiter") - # Note: __init_subclass__ is inherited from BaseEvaluator and handles metrics tracking - - def model_post_init(self, __context: Any): - """Post-initialization hook for Pydantic models.""" - # Ensure config is set up for legacy evaluators - super().model_post_init(__context) - - @classmethod - def get_evaluator_id(cls) -> str: - """Get the evaluator id. - - For legacy evaluators, this returns a placeholder. Actual evaluator instances - have an 'id' field that identifies them. - """ - return "legacy-evaluator" - async def validate_and_evaluate_criteria( self, agent_execution: AgentExecution, evaluation_criteria: LegacyEvaluationCriteria, ) -> EvaluationResult: - """Evaluate the given data and return a result from a raw evaluation criteria.""" + """Validate criteria and evaluate, using line-by-line mode if configured.""" criteria = self.validate_evaluation_criteria(evaluation_criteria) - - # Check if line-by-line evaluation is enabled if self.line_by_line_evaluation: return await self._evaluate_line_by_line(agent_execution, criteria) - return await self.evaluate(agent_execution, criteria) async def _evaluate_line_by_line( @@ -128,22 +63,11 @@ async def _evaluate_line_by_line( agent_execution: AgentExecution, evaluation_criteria: LegacyEvaluationCriteria, ) -> EvaluationResult: - """Evaluate output line-by-line and aggregate results. - - Args: - agent_execution: The execution details - evaluation_criteria: The evaluation criteria - - Returns: - Aggregated NumericEvaluationResult with line-by-line details - """ from .line_by_line_utils import build_line_by_line_result, evaluate_lines - # Extract actual and expected outputs actual_output = self._get_actual_output(agent_execution) expected_output = evaluation_criteria.expected_output - # Split into lines using utility function actual_lines = split_into_lines( actual_output, self.line_delimiter, self.target_output_key ) @@ -151,7 +75,6 @@ async def _evaluate_line_by_line( expected_output, self.line_delimiter, self.target_output_key ) - # Create function to build line criteria def create_line_criteria(expected_line: str) -> LegacyEvaluationCriteria: from .line_by_line_utils import wrap_line_in_structure @@ -163,7 +86,6 @@ def create_line_criteria(expected_line: str) -> LegacyEvaluationCriteria: expected_agent_behavior=evaluation_criteria.expected_agent_behavior, ) - # Evaluate all lines using utility function line_details, line_results = await evaluate_lines( actual_lines=actual_lines, expected_lines=expected_lines, @@ -173,7 +95,6 @@ def create_line_criteria(expected_line: str) -> LegacyEvaluationCriteria: create_line_criteria_fn=create_line_criteria, ) - # Build and return the aggregated result using utility function return build_line_by_line_result( line_details=line_details, line_results=line_results, @@ -182,54 +103,29 @@ def create_line_criteria(expected_line: str) -> LegacyEvaluationCriteria: ) def _get_actual_output(self, agent_execution: AgentExecution) -> Any: - """Extract actual output from agent execution. - - If the output is a job attachment URI, downloads the attachment - and returns its content as a string. - - Args: - agent_execution: The agent execution - - Returns: - The actual output (either the full agent_output or a specific key) - """ agent_output = agent_execution.agent_output - # If target_output_key is "*", return full output if self.target_output_key == "*": result = agent_output - # Otherwise, extract specific key elif isinstance(agent_output, dict) and self.target_output_key in agent_output: result = agent_output[self.target_output_key] else: - # Fallback to full output result = agent_output - # Check if result is a job attachment URI and download if so if is_job_attachment_uri(result): - # At this point we know result is a string assert isinstance(result, str) attachment_id = extract_attachment_id(result) result = download_attachment_as_string(attachment_id) return result - @abstractmethod - async def evaluate( - self, - agent_execution: AgentExecution, - evaluation_criteria: LegacyEvaluationCriteria, - ) -> EvaluationResult: - """Evaluate the given data and return a result. - - Args: - agent_execution: The execution details containing: - - agent_input: The input received by the agent - - actual_output: The actual output from the agent - - spans: The execution spans to use for the evaluation - evaluation_criteria: The criteria to evaluate (legacy evaluators accept any type) - - Returns: - EvaluationResult containing the score and details - """ - pass + +__all__ = [ + "BaseLegacyEvaluator", + "LegacyEvaluationCriteria", + "LegacyEvaluatorConfig", + "track_evaluation_metrics", + "BaseEvaluationCriteria", + "BaseEvaluatorConfig", + "GenericBaseEvaluator", +] diff --git a/packages/uipath/src/uipath/eval/evaluators/binary_classification_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/binary_classification_evaluator.py index d56509228..de6fe4f60 100644 --- a/packages/uipath/src/uipath/eval/evaluators/binary_classification_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/binary_classification_evaluator.py @@ -1,132 +1,13 @@ -"""Binary classification evaluator for agent outputs. +"""Re-exported from uipath-eval.""" -Evaluates binary classification by comparing predicted vs expected class. -Per-datapoint score is 1.0 (correct) or 0.0 (incorrect). The reduce_scores -method reads predicted/expected from justification details to build -TP/FP/FN/TN counts and compute precision, recall, or F-score. -""" - -from typing import Literal - -from ..models import ( - AgentExecution, - EvaluationResult, - EvaluatorType, - NumericEvaluationResult, -) -from ..models.models import ( - EvaluationResultDto, - UiPathEvaluationError, - UiPathEvaluationErrorCategory, +from uipath_eval.evaluators.binary_classification_evaluator import ( + BinaryClassificationEvaluationCriteria, + BinaryClassificationEvaluator, + BinaryClassificationEvaluatorConfig, ) -from .base_evaluator import BaseEvaluationCriteria, BaseEvaluatorJustification -from .output_evaluator import ( - BaseOutputEvaluator, - OutputEvaluatorConfig, -) - - -class BinaryClassificationEvaluationCriteria(BaseEvaluationCriteria): - """Per-datapoint criteria: which class this sample should belong to.""" - - expected_class: str - - -class BinaryClassificationEvaluatorConfig( - OutputEvaluatorConfig[BinaryClassificationEvaluationCriteria] -): - """Configuration for the binary classification evaluator.""" - - name: str = "BinaryClassificationEvaluator" - positive_class: str - metric_type: Literal["precision", "recall", "f-score"] = "precision" - f_value: float = 1.0 - - -class BinaryClassificationEvaluator( - BaseOutputEvaluator[ - BinaryClassificationEvaluationCriteria, - BinaryClassificationEvaluatorConfig, - BaseEvaluatorJustification, - ] -): - """Binary classification evaluator with precision/recall/F-score aggregation. - - Per-datapoint scores are 1.0 (correct) or 0.0 (incorrect). The reduce_scores - method reads predicted/expected from justification details to compute metrics. - """ - - @classmethod - def get_evaluator_id(cls) -> str: - """Get the evaluator id.""" - return EvaluatorType.BINARY_CLASSIFICATION.value - - async def evaluate( - self, - agent_execution: AgentExecution, - evaluation_criteria: BinaryClassificationEvaluationCriteria, - ) -> EvaluationResult: - """Evaluate binary classification by comparing predicted vs expected class.""" - predicted_class = str(self._get_actual_output(agent_execution)).lower() - expected_class = evaluation_criteria.expected_class.lower() - positive_class = self.evaluator_config.positive_class.lower() - - if not positive_class: - raise UiPathEvaluationError( - code="INVALID_POSITIVE_CLASS", - title="Positive class is empty", - detail="positive_class must be a non-empty string", - category=UiPathEvaluationErrorCategory.USER, - ) - - score = 1.0 if predicted_class == expected_class else 0.0 - - justification = self.validate_justification( - { - "expected": expected_class, - "actual": predicted_class, - } - ) - return NumericEvaluationResult(score=score, details=justification) - - def reduce_scores(self, results: list[EvaluationResultDto]) -> float: - """Compute precision, recall, or F-score from per-datapoint results.""" - if not results: - return 0.0 - - positive_class = self.evaluator_config.positive_class.lower() - tp = fp = fn = 0 - - for r in results: - if isinstance(r.details, BaseEvaluatorJustification): - details = r.details - elif isinstance(r.details, dict): - try: - details = BaseEvaluatorJustification.model_validate(r.details) - except Exception: - continue - else: - continue - pred = details.actual - exp = details.expected - if pred == positive_class and exp == positive_class: - tp += 1 - elif pred == positive_class: - fp += 1 - elif exp == positive_class: - fn += 1 - - metric_type = self.evaluator_config.metric_type - if metric_type == "precision": - return tp / (tp + fp) if (tp + fp) > 0 else 0.0 - elif metric_type == "recall": - return tp / (tp + fn) if (tp + fn) > 0 else 0.0 - elif metric_type == "f-score": - p = tp / (tp + fp) if (tp + fp) > 0 else 0.0 - rec = tp / (tp + fn) if (tp + fn) > 0 else 0.0 - beta_sq = self.evaluator_config.f_value**2 - denom = beta_sq * p + rec - return (1 + beta_sq) * p * rec / denom if denom > 0 else 0.0 - else: - return 0.0 +__all__ = [ + "BinaryClassificationEvaluationCriteria", + "BinaryClassificationEvaluator", + "BinaryClassificationEvaluatorConfig", +] diff --git a/packages/uipath/src/uipath/eval/evaluators/contains_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/contains_evaluator.py index c002b47d6..a6ccefc80 100644 --- a/packages/uipath/src/uipath/eval/evaluators/contains_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/contains_evaluator.py @@ -1,90 +1,13 @@ -"""Contains evaluator for agent outputs.""" +"""Re-exported from uipath-eval.""" -from ..models import ( - AgentExecution, - EvaluationResult, - EvaluatorType, - NumericEvaluationResult, +from uipath_eval.evaluators.contains_evaluator import ( + ContainsEvaluationCriteria, + ContainsEvaluator, + ContainsEvaluatorConfig, ) -from .base_evaluator import BaseEvaluationCriteria, BaseEvaluatorJustification -from .output_evaluator import ( - BaseOutputEvaluator, - OutputEvaluatorConfig, -) - - -class ContainsEvaluationCriteria(BaseEvaluationCriteria): - """Evaluation criteria for the contains evaluator.""" - - search_text: str - - -class ContainsEvaluatorConfig(OutputEvaluatorConfig[ContainsEvaluationCriteria]): - """Configuration for the contains evaluator.""" - - name: str = "ContainsEvaluator" - case_sensitive: bool = False - negated: bool = False - - -class ContainsEvaluator( - BaseOutputEvaluator[ - ContainsEvaluationCriteria, ContainsEvaluatorConfig, BaseEvaluatorJustification - ] -): - """Evaluator that checks if the actual output contains the expected output. - - This evaluator returns True if the actual output contains the expected output, - and False otherwise. It supports case sensitivity and negation options. - """ - - @classmethod - def get_evaluator_id(cls) -> str: - """Get the evaluator id.""" - return EvaluatorType.CONTAINS.value - - async def evaluate( - self, - agent_execution: AgentExecution, - evaluation_criteria: ContainsEvaluationCriteria, - ) -> EvaluationResult: - """Evaluate whether actual output contains the expected output. - - Args: - agent_execution: The execution details containing: - - agent_input: The input received by the agent - - agent_output: The actual output from the agent - - agent_trace: The execution spans to use for the evaluation - evaluation_criteria: The criteria to evaluate - - Returns: - EvaluationResult: Boolean result indicating if output contains expected value (True/False) - """ - actual_output = str(self._get_actual_output(agent_execution)) - expected_output = str(self._get_expected_output(evaluation_criteria)) - - if not self.evaluator_config.case_sensitive: - actual_output = actual_output.lower() - expected_output = expected_output.lower() - - is_contains = expected_output in actual_output - - if self.evaluator_config.negated: - is_contains = not is_contains - - validated_justification = self.validate_justification( - { - "expected": expected_output, - "actual": actual_output, - } - ) - return NumericEvaluationResult( - score=float(is_contains), - details=validated_justification, - ) - def _get_expected_output( - self, evaluation_criteria: ContainsEvaluationCriteria - ) -> str: - """Get the expected output from the evaluation criteria.""" - return evaluation_criteria.search_text +__all__ = [ + "ContainsEvaluationCriteria", + "ContainsEvaluator", + "ContainsEvaluatorConfig", +] diff --git a/packages/uipath/src/uipath/eval/evaluators/evaluator_factory.py b/packages/uipath/src/uipath/eval/evaluators/evaluator_factory.py index 6a80caac6..b412f9b70 100644 --- a/packages/uipath/src/uipath/eval/evaluators/evaluator_factory.py +++ b/packages/uipath/src/uipath/eval/evaluators/evaluator_factory.py @@ -11,7 +11,6 @@ from ..constants import CUSTOM_EVALUATOR_PREFIX, EVALS_FOLDER from . import ( BaseEvaluator, - BaseLegacyEvaluator, LegacyContextPrecisionEvaluator, LegacyFaithfulnessEvaluator, LegacyLlmAsAJudgeEvaluator, @@ -185,7 +184,7 @@ def _create_custom_coded_evaluator_internal( def _create_legacy_evaluator_internal( data: dict[str, Any], agent_model: str | None = None, - ) -> BaseLegacyEvaluator[Any]: + ) -> LegacyEvaluator: """Create an evaluator instance from configuration data. Args: diff --git a/packages/uipath/src/uipath/eval/evaluators/exact_match_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/exact_match_evaluator.py index 0f1b3e8e8..e380850b0 100644 --- a/packages/uipath/src/uipath/eval/evaluators/exact_match_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/exact_match_evaluator.py @@ -1,12 +1,9 @@ """Exact match evaluator for agent outputs.""" -from ..models import ( - AgentExecution, - EvaluationResult, - EvaluatorType, - NumericEvaluationResult, -) -from .base_evaluator import BaseEvaluatorJustification +from uipath_eval.evaluators.base_evaluator import BaseEvaluatorJustification +from uipath_eval.models import EvaluatorType + +from ..models import AgentExecution, EvaluationResult, NumericEvaluationResult from .output_evaluator import ( OutputEvaluationCriteria, OutputEvaluator, @@ -24,15 +21,12 @@ class ExactMatchEvaluatorConfig(OutputEvaluatorConfig[OutputEvaluationCriteria]) class ExactMatchEvaluator( OutputEvaluator[ - OutputEvaluationCriteria, ExactMatchEvaluatorConfig, BaseEvaluatorJustification + OutputEvaluationCriteria, + ExactMatchEvaluatorConfig, + BaseEvaluatorJustification, ] ): - """Evaluator that performs exact structural matching between expected and actual outputs. - - This evaluator returns True if the actual output exactly matches the expected output - after canonical JSON normalization, and False otherwise. Numbers are normalized - to floats for consistent comparison. - """ + """Evaluator that performs exact structural matching between expected and actual outputs.""" @classmethod def get_evaluator_id(cls) -> str: @@ -44,18 +38,7 @@ async def evaluate( agent_execution: AgentExecution, evaluation_criteria: OutputEvaluationCriteria, ) -> EvaluationResult: - """Evaluate whether actual output exactly matches expected output. - - Args: - agent_execution: The execution details containing: - - agent_input: The input received by the agent - - agent_output: The actual output from the agent - - agent_trace: The execution spans to use for the evaluation - evaluation_criteria: The criteria to evaluate - - Returns: - EvaluationResult: Boolean result indicating exact match (True/False) - """ + """Evaluate whether actual output exactly matches expected output.""" actual_output = self._get_actual_output(agent_execution) expected_output = self._get_expected_output(evaluation_criteria) @@ -82,3 +65,9 @@ async def evaluate( score=float(is_exact_match), details=validated_justification, ) + + +__all__ = [ + "ExactMatchEvaluator", + "ExactMatchEvaluatorConfig", +] diff --git a/packages/uipath/src/uipath/eval/evaluators/json_similarity_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/json_similarity_evaluator.py index 552194f2e..11371c5ba 100644 --- a/packages/uipath/src/uipath/eval/evaluators/json_similarity_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/json_similarity_evaluator.py @@ -1,200 +1,13 @@ -"""JSON similarity evaluator for flexible structural comparison of outputs.""" +"""JSON similarity evaluator — re-exported from uipath-eval.""" -import math -from typing import Any, Tuple - -from ..models import ( - AgentExecution, - EvaluationResult, - EvaluatorType, - NumericEvaluationResult, -) -from .base_evaluator import BaseEvaluatorJustification -from .output_evaluator import ( - OutputEvaluationCriteria, - OutputEvaluator, - OutputEvaluatorConfig, +from uipath_eval.evaluators.json_similarity_evaluator import ( + JsonSimilarityEvaluator, + JsonSimilarityEvaluatorConfig, + JsonSimilarityJustification, ) - -class JsonSimilarityJustification(BaseEvaluatorJustification): - """Justification for the JSON similarity evaluator.""" - - matched_leaves: float - total_leaves: float - - -class JsonSimilarityEvaluatorConfig(OutputEvaluatorConfig[OutputEvaluationCriteria]): - """Configuration for the json similarity evaluator.""" - - name: str = "JsonSimilarityEvaluator" - - -class JsonSimilarityEvaluator( - OutputEvaluator[ - OutputEvaluationCriteria, - JsonSimilarityEvaluatorConfig, - JsonSimilarityJustification, - ] -): - """Deterministic evaluator that scores structural JSON similarity between expected and actual output. - - Compares expected versus actual JSON-like structures and returns a - numerical score in the range [0, 100]. The comparison is token-based - and tolerant for numbers and strings (via Levenshtein distance). - """ - - @classmethod - def get_evaluator_id(cls) -> str: - """Get the evaluator id.""" - return EvaluatorType.JSON_SIMILARITY.value - - async def evaluate( - self, - agent_execution: AgentExecution, - evaluation_criteria: OutputEvaluationCriteria, - ) -> EvaluationResult: - """Evaluate similarity between expected and actual JSON outputs. - - Uses token-based comparison with tolerance for numeric differences - and Levenshtein distance for string similarity. - - agent_execution: The execution details containing: - - agent_input: The input received by the agent - - actual_output: The actual output from the agent - - spans: The execution spans to use for the evaluation - evaluation_criteria: The criteria to evaluate - - Returns: - EvaluationResult: Numerical score between 0-100 indicating similarity - """ - expected_output = self._get_expected_output(evaluation_criteria) - actual_output = self._get_actual_output(agent_execution) - score, justification = self._compare_json( - expected_output, - actual_output, - ) - validated_justification = self.validate_justification(justification) - return NumericEvaluationResult( - score=score, - details=validated_justification, - ) - - def _compare_json(self, expected: Any, actual: Any) -> tuple[float, dict[str, Any]]: - matched_leaves, total_leaves = self._compare_tokens(expected, actual) - if total_leaves == 0: - sim = 1.0 - else: - sim = matched_leaves / total_leaves - return ( - max(0.0, min(1.0, sim)), - { - "expected": str(expected), - "actual": str(actual), - "matched_leaves": matched_leaves, - "total_leaves": total_leaves, - }, - ) - - def _compare_tokens( - self, expected_token: Any, actual_token: Any - ) -> Tuple[float, float]: - if self._is_number(expected_token) and self._is_number(actual_token): - return self._compare_numbers(float(expected_token), float(actual_token)) - - if type(expected_token) is not type(actual_token): - return 0.0, self._count_leaves(expected_token) - - if isinstance(expected_token, dict): - matched_leaves = total_leaves = 0.0 - # Only expected keys count - for expected_key, expected_value in expected_token.items(): - if isinstance(actual_token, dict) and expected_key in actual_token: - matched, total = self._compare_tokens( - expected_value, actual_token[expected_key] - ) - else: - matched, total = (0.0, self._count_leaves(expected_value)) - matched_leaves += matched - total_leaves += total - return matched_leaves, total_leaves - - if isinstance(expected_token, list): - matched_leaves = total_leaves = 0.0 - common_length = min(len(expected_token), len(actual_token)) - for index in range(common_length): - matched, total = self._compare_tokens( - expected_token[index], actual_token[index] - ) - matched_leaves += matched - total_leaves += total - for index in range(common_length, len(expected_token)): - total_leaves += self._count_leaves(expected_token[index]) - return (matched_leaves, total_leaves) - - if isinstance(expected_token, bool): - return (1.0, 1.0) if expected_token == actual_token else (0.0, 1.0) - - if isinstance(expected_token, str): - return self._compare_strings(expected_token, actual_token) - - return (1.0, 1.0) if str(expected_token) == str(actual_token) else (0.0, 1.0) - - def _compare_numbers( - self, expected_number: float, actual_number: float - ) -> Tuple[float, float]: - total = 1.0 - if math.isclose(expected_number, 0.0, abs_tol=1e-12): - matched = 1.0 if math.isclose(actual_number, 0.0, abs_tol=1e-12) else 0.0 - else: - ratio = abs(expected_number - actual_number) / abs(expected_number) - matched = max(0.0, min(1.0, 1.0 - ratio)) - return matched, total - - def _compare_strings( - self, expected_string: str, actual_string: str - ) -> Tuple[float, float]: - total = 1.0 - if not expected_string and not actual_string: - return 1.0, total - distance = self._levenshtein(expected_string, actual_string) - max_length = max(len(expected_string), len(actual_string)) - similarity = 1.0 - (distance / max_length) if max_length else 1.0 - similarity = max(0.0, min(1.0, similarity)) - return similarity, total - - def _count_leaves(self, token_node: Any) -> float: - if isinstance(token_node, dict): - return sum( - self._count_leaves(child_value) for child_value in token_node.values() - ) - if isinstance(token_node, list): - return sum(self._count_leaves(child_value) for child_value in token_node) - return 1.0 - - def _levenshtein(self, source_text: str, target_text: str) -> int: - if not source_text: - return len(target_text) - if not target_text: - return len(source_text) - source_len, target_len = len(source_text), len(target_text) - distance_matrix = [[0] * (target_len + 1) for _ in range(source_len + 1)] - for row_idx in range(source_len + 1): - distance_matrix[row_idx][0] = row_idx - for col_idx in range(target_len + 1): - distance_matrix[0][col_idx] = col_idx - for row_idx in range(1, source_len + 1): - for col_idx in range(1, target_len + 1): - substitution_cost = ( - 0 if source_text[row_idx - 1] == target_text[col_idx - 1] else 1 - ) - distance_matrix[row_idx][col_idx] = min( - distance_matrix[row_idx - 1][col_idx] + 1, # deletion - distance_matrix[row_idx][col_idx - 1] + 1, # insertion - distance_matrix[row_idx - 1][col_idx - 1] - + substitution_cost, # substitution - ) - return distance_matrix[source_len][target_len] - - def _is_number(self, value: Any) -> bool: - return isinstance(value, (int, float)) and not isinstance(value, bool) +__all__ = [ + "JsonSimilarityEvaluator", + "JsonSimilarityEvaluatorConfig", + "JsonSimilarityJustification", +] diff --git a/packages/uipath/src/uipath/eval/evaluators/legacy_exact_match_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/legacy_exact_match_evaluator.py index 42ffae047..2ba6f7622 100644 --- a/packages/uipath/src/uipath/eval/evaluators/legacy_exact_match_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/legacy_exact_match_evaluator.py @@ -1,17 +1,33 @@ -"""Exact match evaluator for binary pass/fail evaluation of agent outputs.""" +"""Exact match evaluator with line-by-line evaluation support.""" -from uipath.eval.models import BooleanEvaluationResult, EvaluationResult +from typing import Any + +from pydantic import Field +from uipath_eval.evaluators.base_legacy_evaluator import ( + LegacyEvaluationCriteria, + LegacyEvaluatorConfig, +) +from uipath_eval.evaluators.legacy_deterministic_evaluator_base import ( + BaseLegacyDeterministicEvaluator, +) from .._helpers.output_path import resolve_output_path -from ..models.models import AgentExecution -from .base_legacy_evaluator import LegacyEvaluationCriteria, LegacyEvaluatorConfig -from .legacy_deterministic_evaluator_base import BaseLegacyDeterministicEvaluator +from ..models.models import AgentExecution, EvaluationResult +from .line_by_line_utils import ( + aggregate_line_scores, + build_line_by_line_result, + evaluate_lines, + split_into_lines, + wrap_line_in_structure, +) class LegacyExactMatchEvaluatorConfig(LegacyEvaluatorConfig): """Configuration for legacy exact-match evaluators.""" name: str = "LegacyExactMatchEvaluator" + line_by_line_evaluation: bool = Field(default=False, alias="lineByLineEvaluation") + line_delimiter: str = Field(default="\n", alias="lineDelimiter") class LegacyExactMatchEvaluator( @@ -19,11 +35,13 @@ class LegacyExactMatchEvaluator( ): """Evaluator that performs exact structural matching between expected and actual outputs. - This evaluator returns True if the actual output exactly matches the expected output - after canonical JSON normalization, and False otherwise. Numbers are normalized - to floats for consistent comparison. + Supports optional line-by-line mode where the output is split by a delimiter and + each line is evaluated independently, returning an aggregated numeric score. """ + line_by_line_evaluation: bool = Field(default=False, alias="lineByLineEvaluation") + line_delimiter: str = Field(default="\n", alias="lineDelimiter") + async def evaluate( self, agent_execution: AgentExecution, @@ -32,17 +50,27 @@ async def evaluate( """Evaluate whether actual output exactly matches expected output. Args: - agent_execution: The execution details containing: - - agent_input: The input received by the agent - - actual_output: The actual output from the agent - - spans: The execution spans to use for the evaluation + agent_execution: The execution details evaluation_criteria: The criteria to evaluate Returns: - EvaluationResult: Boolean result indicating exact match (True/False) + EvaluationResult: Boolean (plain match) or NumericEvaluationResult (line-by-line) """ - actual_output = agent_execution.agent_output - expected_output = evaluation_criteria.expected_output + if self.line_by_line_evaluation: + return await self._evaluate_line_by_line( + agent_execution, evaluation_criteria + ) + return await self._evaluate_exact(agent_execution, evaluation_criteria) + + async def _evaluate_exact( + self, + agent_execution: AgentExecution, + evaluation_criteria: LegacyEvaluationCriteria, + ) -> EvaluationResult: + from ..models import BooleanEvaluationResult + + actual_output: Any = agent_execution.agent_output + expected_output: Any = evaluation_criteria.expected_output if self.target_output_key and self.target_output_key != "*": if isinstance(actual_output, dict) and isinstance(expected_output, dict): @@ -70,3 +98,51 @@ async def evaluate( score=self._canonical_json(actual_output) == self._canonical_json(expected_output) ) + + async def _evaluate_line_by_line( + self, + agent_execution: AgentExecution, + evaluation_criteria: LegacyEvaluationCriteria, + ) -> EvaluationResult: + actual_lines = split_into_lines( + agent_execution.agent_output, + self.line_delimiter, + self.target_output_key, + ) + expected_lines = split_into_lines( + evaluation_criteria.expected_output, + self.line_delimiter, + self.target_output_key, + ) + + if not actual_lines and not expected_lines: + from ..models import BooleanEvaluationResult + + return BooleanEvaluationResult(score=True) + + line_details, line_results = await evaluate_lines( + actual_lines=actual_lines, + expected_lines=expected_lines, + target_output_key=self.target_output_key, + agent_execution=agent_execution, + evaluate_fn=self._evaluate_exact, + create_line_criteria_fn=lambda expected_line: LegacyEvaluationCriteria( + expected_output=wrap_line_in_structure( + expected_line, self.target_output_key + ), + expected_agent_behavior="", + ), + ) + + if not line_results: + from ..models.models import NumericEvaluationResult + + return NumericEvaluationResult(score=0.0) + + _ = aggregate_line_scores(line_results) + return build_line_by_line_result( + line_details=line_details, + line_results=line_results, + actual_lines=actual_lines, + expected_lines=expected_lines, + ) diff --git a/packages/uipath/src/uipath/eval/evaluators/multiclass_classification_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/multiclass_classification_evaluator.py index 69790c3aa..cf3d5cc0b 100644 --- a/packages/uipath/src/uipath/eval/evaluators/multiclass_classification_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/multiclass_classification_evaluator.py @@ -1,198 +1,13 @@ -"""Multiclass classification evaluator for agent outputs. +"""Re-exported from uipath-eval.""" -Evaluates multiclass classification by comparing predicted vs expected class. -Per-datapoint score is 1.0 (correct) or 0.0 (incorrect). The reduce_scores -method reads predicted/expected from justification details to reconstruct a -confusion matrix and compute precision, recall, or F-score with micro or -macro averaging. -""" - -from typing import Literal - -from ..models import ( - AgentExecution, - EvaluationResult, - EvaluatorType, - NumericEvaluationResult, -) -from ..models.models import ( - EvaluationResultDto, - UiPathEvaluationError, - UiPathEvaluationErrorCategory, -) -from .base_evaluator import BaseEvaluationCriteria, BaseEvaluatorJustification -from .output_evaluator import ( - BaseOutputEvaluator, - OutputEvaluatorConfig, +from uipath_eval.evaluators.multiclass_classification_evaluator import ( + MulticlassClassificationEvaluationCriteria, + MulticlassClassificationEvaluator, + MulticlassClassificationEvaluatorConfig, ) - -class MulticlassClassificationEvaluationCriteria(BaseEvaluationCriteria): - """Per-datapoint criteria: which class this sample should belong to.""" - - expected_class: str - - -class MulticlassClassificationEvaluatorConfig( - OutputEvaluatorConfig[MulticlassClassificationEvaluationCriteria] -): - """Configuration for the multiclass classification evaluator.""" - - name: str = "MulticlassClassificationEvaluator" - classes: list[str] - metric_type: Literal["precision", "recall", "f-score"] = "f-score" - averaging: Literal["micro", "macro"] = "macro" - f_value: float = 1.0 - - -class MulticlassClassificationEvaluator( - BaseOutputEvaluator[ - MulticlassClassificationEvaluationCriteria, - MulticlassClassificationEvaluatorConfig, - BaseEvaluatorJustification, - ] -): - """Multiclass classification evaluator with micro/macro averaging. - - Per-datapoint scores are 1.0 (correct) or 0.0 (incorrect). The reduce_scores - method reads predicted/expected from justification details to reconstruct the - confusion matrix and compute the configured metric. - """ - - @classmethod - def get_evaluator_id(cls) -> str: - """Get the evaluator id.""" - return EvaluatorType.MULTICLASS_CLASSIFICATION.value - - async def evaluate( - self, - agent_execution: AgentExecution, - evaluation_criteria: MulticlassClassificationEvaluationCriteria, - ) -> EvaluationResult: - """Evaluate multiclass classification by comparing predicted vs expected class.""" - predicted_class = str(self._get_actual_output(agent_execution)).lower() - expected_class = evaluation_criteria.expected_class.lower() - classes = [c.lower() for c in self.evaluator_config.classes] - - if expected_class not in classes: - raise UiPathEvaluationError( - code="INVALID_EXPECTED_CLASS", - title="Expected class not in configured classes", - detail=f"Expected class '{expected_class}' is not in the configured classes: {classes}", - category=UiPathEvaluationErrorCategory.USER, - ) - - if predicted_class not in classes: - raise UiPathEvaluationError( - code="INVALID_PREDICTED_CLASS", - title="Predicted class not in configured classes", - detail=f"Predicted class '{predicted_class}' is not in the configured classes: {classes}", - category=UiPathEvaluationErrorCategory.USER, - ) - - score = 1.0 if predicted_class == expected_class else 0.0 - - justification = self.validate_justification( - { - "expected": expected_class, - "actual": predicted_class, - } - ) - return NumericEvaluationResult(score=score, details=justification) - - def reduce_scores(self, results: list[EvaluationResultDto]) -> float: - """Reconstruct confusion matrix from details and compute the configured metric.""" - if not results: - return 0.0 - - classes = [c.lower() for c in self.evaluator_config.classes] - k = len(classes) - metric_type = self.evaluator_config.metric_type - averaging = self.evaluator_config.averaging - f_value = self.evaluator_config.f_value - - # Reconstruct confusion matrix: confusion[pred_idx][exp_idx] - confusion = [[0] * k for _ in range(k)] - for r in results: - if isinstance(r.details, BaseEvaluatorJustification): - details = r.details - elif isinstance(r.details, dict): - try: - details = BaseEvaluatorJustification.model_validate(r.details) - except Exception: - continue - else: - continue - pred = details.actual - exp = details.expected - if pred in classes and exp in classes: - confusion[classes.index(pred)][classes.index(exp)] += 1 - - if averaging == "micro": - return _micro_metric(confusion, k, metric_type, f_value) - else: - return _macro_metric(confusion, k, metric_type, f_value) - - -def _micro_metric( - confusion: list[list[int]], - k: int, - metric_type: str, - f_value: float, -) -> float: - """Compute micro-averaged metric from confusion matrix.""" - total_tp = sum(confusion[i][i] for i in range(k)) - # For micro-averaging, sum TP/FP/FN across all classes - total_fp = sum( - sum(confusion[i][j] for j in range(k)) - confusion[i][i] for i in range(k) - ) - total_fn = sum( - sum(confusion[j][i] for j in range(k)) - confusion[i][i] for i in range(k) - ) - - if metric_type == "precision": - denom = total_tp + total_fp - return total_tp / denom if denom > 0 else 0.0 - elif metric_type == "recall": - denom = total_tp + total_fn - return total_tp / denom if denom > 0 else 0.0 - elif metric_type == "f-score": - p = total_tp / (total_tp + total_fp) if (total_tp + total_fp) > 0 else 0.0 - rec = total_tp / (total_tp + total_fn) if (total_tp + total_fn) > 0 else 0.0 - beta_sq = f_value**2 - f_denom = beta_sq * p + rec - return (1 + beta_sq) * p * rec / f_denom if f_denom > 0 else 0.0 - return 0.0 - - -def _macro_metric( - confusion: list[list[int]], - k: int, - metric_type: str, - f_value: float, -) -> float: - """Compute macro-averaged metric from confusion matrix.""" - per_class_metrics: list[float] = [] - - for c in range(k): - tp = confusion[c][c] - fp = sum(confusion[c][j] for j in range(k)) - tp - fn = sum(confusion[j][c] for j in range(k)) - tp - - if metric_type == "precision": - denom = tp + fp - per_class_metrics.append(tp / denom if denom > 0 else 0.0) - elif metric_type == "recall": - denom = tp + fn - per_class_metrics.append(tp / denom if denom > 0 else 0.0) - elif metric_type == "f-score": - p = tp / (tp + fp) if (tp + fp) > 0 else 0.0 - rec = tp / (tp + fn) if (tp + fn) > 0 else 0.0 - beta_sq = f_value**2 - f_denom = beta_sq * p + rec - f_score = (1 + beta_sq) * p * rec / f_denom if f_denom > 0 else 0.0 - per_class_metrics.append(f_score) - - if not per_class_metrics: - return 0.0 - return sum(per_class_metrics) / len(per_class_metrics) +__all__ = [ + "MulticlassClassificationEvaluationCriteria", + "MulticlassClassificationEvaluator", + "MulticlassClassificationEvaluatorConfig", +] diff --git a/packages/uipath/src/uipath/eval/evaluators/output_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/output_evaluator.py index 27dd05d9f..d09840377 100644 --- a/packages/uipath/src/uipath/eval/evaluators/output_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/output_evaluator.py @@ -5,6 +5,9 @@ from typing import TYPE_CHECKING, Any, TypeVar, Union from pydantic import BaseModel, Field +from uipath_eval.evaluators.output_evaluator import ( + OutputEvaluationCriteria as OutputEvaluationCriteria, +) from .._helpers.output_path import resolve_output_path from ..models import AgentExecution @@ -69,12 +72,6 @@ class LineByLineEvaluationResult(BaseModel): aggregation_method: AggregationMethod = AggregationMethod.AVERAGE -class OutputEvaluationCriteria(BaseEvaluationCriteria): - """Base class for all output evaluation criteria.""" - - expected_output: dict[str, Any] | str = Field(..., alias="expectedOutput") - - T = TypeVar("T", bound=BaseEvaluationCriteria) T_OutputCriteria = TypeVar("T_OutputCriteria", bound=OutputEvaluationCriteria) diff --git a/packages/uipath/src/uipath/eval/evaluators/tool_call_args_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/tool_call_args_evaluator.py index 2703e3c76..b67a8d387 100644 --- a/packages/uipath/src/uipath/eval/evaluators/tool_call_args_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/tool_call_args_evaluator.py @@ -1,82 +1,15 @@ -"""Tool call order evaluator for validating correct sequence of tool calls.""" +"""Re-exported from uipath-eval.""" -from .._helpers.evaluators_helpers import ( - extract_tool_calls, - tool_calls_args_score, +from uipath_eval.evaluators.tool_call_args_evaluator import ( + ToolCallArgsEvaluationCriteria, + ToolCallArgsEvaluator, + ToolCallArgsEvaluatorConfig, + ToolCallArgsEvaluatorJustification, ) -from ..models import AgentExecution, EvaluationResult, NumericEvaluationResult, ToolCall -from ..models.models import EvaluatorType -from .base_evaluator import ( - BaseEvaluationCriteria, - BaseEvaluator, - BaseEvaluatorConfig, - BaseEvaluatorJustification, -) - - -class ToolCallArgsEvaluationCriteria(BaseEvaluationCriteria): - """Evaluation criteria for the tool call order evaluator.""" - - # TODO: name field of ToolCall needs to be validated such that it contains only the tools available - tool_calls: list[ToolCall] - - -class ToolCallArgsEvaluatorConfig(BaseEvaluatorConfig[ToolCallArgsEvaluationCriteria]): - """Configuration for the tool call count evaluator.""" - - name: str = "ToolCallArgsEvaluator" - strict: bool = False - subset: bool = False - - -class ToolCallArgsEvaluatorJustification(BaseEvaluatorJustification): - """Justification for the tool call args evaluator.""" - - explained_tool_calls_args: dict[str, str] - - -class ToolCallArgsEvaluator( - BaseEvaluator[ - ToolCallArgsEvaluationCriteria, - ToolCallArgsEvaluatorConfig, - ToolCallArgsEvaluatorJustification, - ] -): - """Evaluator that checks if the tool calls are in the correct order. - - This evaluator returns True if the tool calls are in the correct order, and False otherwise. - """ - - @classmethod - def get_evaluator_id(cls) -> str: - """Get the evaluator id.""" - return EvaluatorType.TOOL_CALL_ARGS.value - - async def evaluate( - self, - agent_execution: AgentExecution, - evaluation_criteria: ToolCallArgsEvaluationCriteria, - ) -> EvaluationResult: - """Evaluate if the tool calls are in the correct order. - Args: - agent_execution: The execution details containing: - - agent_input: The input received by the agent - - agent_output: The final output of the agent - - agent_trace: The execution spans to use for the evaluation - evaluation_criteria: The criteria to evaluate - Returns: - EvaluationResult: Boolean result indicating correct tool call order (True/False) - """ - tool_calls_order = extract_tool_calls(agent_execution.agent_trace) - score, justification = tool_calls_args_score( - tool_calls_order, - evaluation_criteria.tool_calls, - self.evaluator_config.strict, - self.evaluator_config.subset, - ) - validated_justification = self.validate_justification(justification) - return NumericEvaluationResult( - score=score, - details=validated_justification, - ) +__all__ = [ + "ToolCallArgsEvaluationCriteria", + "ToolCallArgsEvaluator", + "ToolCallArgsEvaluatorConfig", + "ToolCallArgsEvaluatorJustification", +] diff --git a/packages/uipath/src/uipath/eval/evaluators/tool_call_count_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/tool_call_count_evaluator.py index 11d684ae1..1f8758100 100644 --- a/packages/uipath/src/uipath/eval/evaluators/tool_call_count_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/tool_call_count_evaluator.py @@ -1,87 +1,15 @@ -"""Tool call count evaluator for validating expected tool usage patterns.""" +"""Re-exported from uipath-eval.""" -from collections import Counter - -from .._helpers.evaluators_helpers import ( - extract_tool_calls_names, - tool_calls_count_score, -) -from ..models import AgentExecution, EvaluationResult, NumericEvaluationResult -from ..models.models import EvaluatorType -from .base_evaluator import ( - BaseEvaluationCriteria, - BaseEvaluator, - BaseEvaluatorConfig, - BaseEvaluatorJustification, +from uipath_eval.evaluators.tool_call_count_evaluator import ( + ToolCallCountEvaluationCriteria, + ToolCallCountEvaluator, + ToolCallCountEvaluatorConfig, + ToolCallCountEvaluatorJustification, ) - -class ToolCallCountEvaluationCriteria(BaseEvaluationCriteria): - """Evaluation criteria for the tool call count evaluator.""" - - # TODO: str field needs to be validated against some criteria that allows ">x", "=x", "<=x", "x" - tool_calls_count: dict[str, tuple[str, int]] - - -class ToolCallCountEvaluatorConfig( - BaseEvaluatorConfig[ToolCallCountEvaluationCriteria] -): - """Configuration for the tool call count evaluator.""" - - name: str = "ToolCallCountEvaluator" - strict: bool = False - - -class ToolCallCountEvaluatorJustification(BaseEvaluatorJustification): - """Justification for the tool call count evaluator.""" - - explained_tool_calls_count: dict[str, str] - - -class ToolCallCountEvaluator( - BaseEvaluator[ - ToolCallCountEvaluationCriteria, - ToolCallCountEvaluatorConfig, - ToolCallCountEvaluatorJustification, - ] -): - """Evaluator that checks if the tool calls match the expected count. - - This evaluator returns a score based on how well the actual tool call counts - match the expected counts specified in the criteria. - """ - - @classmethod - def get_evaluator_id(cls) -> str: - """Get the evaluator id.""" - return EvaluatorType.TOOL_CALL_COUNT.value - - async def evaluate( - self, - agent_execution: AgentExecution, - evaluation_criteria: ToolCallCountEvaluationCriteria, - ) -> EvaluationResult: - """Evaluate if the tool calls are in the correct order. - - Args: - agent_execution: The execution details containing: - - agent_input: The input received by the agent - - agent_output: The final output of the agent - - agent_trace: The execution spans to use for the evaluation - evaluation_criteria: The criteria to evaluate - Returns: - EvaluationResult: Boolean result indicating correct tool call order (True/False) - """ - tool_calls_count = Counter( - extract_tool_calls_names(agent_execution.agent_trace) - ) - score, justification = tool_calls_count_score( - tool_calls_count, - evaluation_criteria.tool_calls_count, - self.evaluator_config.strict, - ) - validated_justification = self.validate_justification(justification) - return NumericEvaluationResult( - score=score, - details=validated_justification, - ) +__all__ = [ + "ToolCallCountEvaluationCriteria", + "ToolCallCountEvaluator", + "ToolCallCountEvaluatorConfig", + "ToolCallCountEvaluatorJustification", +] diff --git a/packages/uipath/src/uipath/eval/evaluators/tool_call_order_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/tool_call_order_evaluator.py index 1050ddc76..018c7a789 100644 --- a/packages/uipath/src/uipath/eval/evaluators/tool_call_order_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/tool_call_order_evaluator.py @@ -1,82 +1,15 @@ -"""Tool call order evaluator for validating correct sequence of tool calls.""" +"""Re-exported from uipath-eval.""" -from .._helpers.evaluators_helpers import ( - extract_tool_calls_names, - tool_calls_order_score, +from uipath_eval.evaluators.tool_call_order_evaluator import ( + ToolCallOrderEvaluationCriteria, + ToolCallOrderEvaluator, + ToolCallOrderEvaluatorConfig, + ToolCallOrderEvaluatorJustification, ) -from ..models import AgentExecution, EvaluationResult, NumericEvaluationResult -from ..models.models import EvaluatorType -from .base_evaluator import ( - BaseEvaluationCriteria, - BaseEvaluator, - BaseEvaluatorConfig, - BaseEvaluatorJustification, -) - - -class ToolCallOrderEvaluationCriteria(BaseEvaluationCriteria): - """Evaluation criteria for the tool call order evaluator.""" - - # TODO: str field needs to be validated such that it contains only the tools available - tool_calls_order: list[str] - - -class ToolCallOrderEvaluatorConfig( - BaseEvaluatorConfig[ToolCallOrderEvaluationCriteria] -): - """Configuration for the tool call count evaluator.""" - - name: str = "ToolCallOrderEvaluator" - strict: bool = False - - -class ToolCallOrderEvaluatorJustification(BaseEvaluatorJustification): - """Justification for the tool call order evaluator.""" - - lcs: list[str] - - -class ToolCallOrderEvaluator( - BaseEvaluator[ - ToolCallOrderEvaluationCriteria, - ToolCallOrderEvaluatorConfig, - ToolCallOrderEvaluatorJustification, - ] -): - """Evaluator that checks if the tool calls are in the correct order. - - This evaluator returns True if the tool calls are in the correct order, and False otherwise. - """ - - @classmethod - def get_evaluator_id(cls) -> str: - """Get the evaluator id.""" - return EvaluatorType.TOOL_CALL_ORDER.value - - async def evaluate( - self, - agent_execution: AgentExecution, - evaluation_criteria: ToolCallOrderEvaluationCriteria, - ) -> EvaluationResult: - """Evaluate if the tool calls are in the correct order. - Args: - agent_execution: The execution details containing: - - agent_input: The input received by the agent - - agent_output: The final output of the agent - - agent_trace: The execution spans to use for the evaluation - evaluation_criteria: The criteria to evaluate - Returns: - EvaluationResult: Boolean result indicating correct tool call order (True/False) - """ - tool_calls_order = extract_tool_calls_names(agent_execution.agent_trace) - score, justification = tool_calls_order_score( - tool_calls_order, - evaluation_criteria.tool_calls_order, - self.evaluator_config.strict, - ) - validated_justification = self.validate_justification(justification) - return NumericEvaluationResult( - score=score, - details=validated_justification, - ) +__all__ = [ + "ToolCallOrderEvaluationCriteria", + "ToolCallOrderEvaluator", + "ToolCallOrderEvaluatorConfig", + "ToolCallOrderEvaluatorJustification", +] diff --git a/packages/uipath/src/uipath/eval/evaluators/tool_call_output_evaluator.py b/packages/uipath/src/uipath/eval/evaluators/tool_call_output_evaluator.py index fff139daf..a0b6dc28b 100644 --- a/packages/uipath/src/uipath/eval/evaluators/tool_call_output_evaluator.py +++ b/packages/uipath/src/uipath/eval/evaluators/tool_call_output_evaluator.py @@ -1,87 +1,15 @@ -"""Tool call order evaluator for validating correct sequence of tool calls.""" +"""Re-exported from uipath-eval.""" -from .._helpers.evaluators_helpers import ( - extract_tool_calls_outputs, - tool_calls_output_score, +from uipath_eval.evaluators.tool_call_output_evaluator import ( + ToolCallOutputEvaluationCriteria, + ToolCallOutputEvaluator, + ToolCallOutputEvaluatorConfig, + ToolCallOutputEvaluatorJustification, ) -from ..models import ( - AgentExecution, - EvaluationResult, - NumericEvaluationResult, - ToolOutput, -) -from ..models.models import EvaluatorType -from .base_evaluator import ( - BaseEvaluationCriteria, - BaseEvaluator, - BaseEvaluatorConfig, - BaseEvaluatorJustification, -) - - -class ToolCallOutputEvaluationCriteria(BaseEvaluationCriteria): - """Evaluation criteria for the tool call order evaluator.""" - - # TODO: name field of ToolCall needs to be validated such that it contains only the tools available - tool_outputs: list[ToolOutput] - - -class ToolCallOutputEvaluatorConfig( - BaseEvaluatorConfig[ToolCallOutputEvaluationCriteria] -): - """Configuration for the tool call count evaluator.""" - - name: str = "ToolCallOutputEvaluator" - strict: bool = False - - -class ToolCallOutputEvaluatorJustification(BaseEvaluatorJustification): - """Justification for the tool call output evaluator.""" - - explained_tool_calls_outputs: dict[str, str] - - -class ToolCallOutputEvaluator( - BaseEvaluator[ - ToolCallOutputEvaluationCriteria, - ToolCallOutputEvaluatorConfig, - ToolCallOutputEvaluatorJustification, - ] -): - """Evaluator that checks if the tool calls are in the correct order. - - This evaluator returns True if the tool calls are in the correct order, and False otherwise. - """ - - @classmethod - def get_evaluator_id(cls) -> str: - """Get the evaluator id.""" - return EvaluatorType.TOOL_CALL_OUTPUT.value - - async def evaluate( - self, - agent_execution: AgentExecution, - evaluation_criteria: ToolCallOutputEvaluationCriteria, - ) -> EvaluationResult: - """Evaluate if the tool calls are in the correct order. - Args: - agent_execution: The execution details containing: - - agent_input: The input received by the agent - - agent_output: The final output of the agent - - agent_trace: The execution spans to use for the evaluation - evaluation_criteria: The criteria to evaluate - Returns: - EvaluationResult: Boolean result indicating correct tool call order (True/False) - """ - tool_calls_outputs = extract_tool_calls_outputs(agent_execution.agent_trace) - score, justification = tool_calls_output_score( - tool_calls_outputs, - evaluation_criteria.tool_outputs, - self.evaluator_config.strict, - ) - validated_justification = self.validate_justification(justification) - return NumericEvaluationResult( - score=score, - details=validated_justification, - ) +__all__ = [ + "ToolCallOutputEvaluationCriteria", + "ToolCallOutputEvaluator", + "ToolCallOutputEvaluatorConfig", + "ToolCallOutputEvaluatorJustification", +] diff --git a/packages/uipath/src/uipath/eval/models/__init__.py b/packages/uipath/src/uipath/eval/models/__init__.py index 580ce812a..c4dad1d14 100644 --- a/packages/uipath/src/uipath/eval/models/__init__.py +++ b/packages/uipath/src/uipath/eval/models/__init__.py @@ -1,6 +1,6 @@ """UiPath evaluation module for agent performance assessment.""" -from uipath.eval.models.models import ( +from uipath_eval.models import ( AgentExecution, BooleanEvaluationResult, ErrorEvaluationResult, diff --git a/packages/uipath/src/uipath/eval/models/models.py b/packages/uipath/src/uipath/eval/models/models.py index d2dc26df9..c12343018 100644 --- a/packages/uipath/src/uipath/eval/models/models.py +++ b/packages/uipath/src/uipath/eval/models/models.py @@ -1,378 +1,85 @@ -"""Models for evaluation framework including execution data and evaluation results.""" - -import traceback -from dataclasses import dataclass -from enum import Enum, IntEnum -from typing import Annotated, Any, Literal, Union - -from opentelemetry.sdk.trace import ReadableSpan -from pydantic import BaseModel, ConfigDict, Field, model_serializer -from pydantic.alias_generators import to_camel -from pydantic_core import core_schema - - -class AgentExecution(BaseModel): - """Represents the execution data of an agent for evaluation purposes.""" - - model_config = ConfigDict(arbitrary_types_allowed=True) - - agent_input: dict[str, Any] | None - agent_output: dict[str, Any] | str - agent_trace: list[ReadableSpan] - expected_agent_behavior: str | None = None - simulation_instructions: str = "" - - -class LLMResponse(BaseModel): - """Response from an LLM evaluator.""" - - score: float - justification: str - - -class ScoreType(IntEnum): - """Types of evaluation scores.""" - - BOOLEAN = 0 - NUMERICAL = 1 - ERROR = 2 - - -class BaseEvaluationResult(BaseModel): - """Base class for evaluation results.""" - - details: str | BaseModel | None = None - # this is marked as optional, as it is populated inside the 'measure_execution_time' decorator - evaluation_time: float | None = None - - -class BooleanEvaluationResult(BaseEvaluationResult): - """Result of a boolean evaluation.""" - - score: bool - score_type: Literal[ScoreType.BOOLEAN] = ScoreType.BOOLEAN - - -class NumericEvaluationResult(BaseEvaluationResult): - """Result of a numerical evaluation.""" - - score: float - score_type: Literal[ScoreType.NUMERICAL] = ScoreType.NUMERICAL - - -class ErrorEvaluationResult(BaseEvaluationResult): - """Result of an error evaluation.""" - - score: float = 0.0 - score_type: Literal[ScoreType.ERROR] = ScoreType.ERROR - - -EvaluationResult = Annotated[ - Union[BooleanEvaluationResult, NumericEvaluationResult, ErrorEvaluationResult], - Field(discriminator="score_type"), +"""Re-exports from uipath_eval for backward compatibility.""" + +from uipath_eval.models.models import ( + AgentExecution as AgentExecution, +) +from uipath_eval.models.models import ( + BaseEvaluationResult as BaseEvaluationResult, +) +from uipath_eval.models.models import ( + BooleanEvaluationResult as BooleanEvaluationResult, +) +from uipath_eval.models.models import ( + ErrorEvaluationResult as ErrorEvaluationResult, +) +from uipath_eval.models.models import ( + EvalItemResult as EvalItemResult, +) +from uipath_eval.models.models import ( + EvaluationResult as EvaluationResult, +) +from uipath_eval.models.models import ( + EvaluationResultDto as EvaluationResultDto, +) +from uipath_eval.models.models import ( + EvaluatorType as EvaluatorType, +) +from uipath_eval.models.models import ( + LegacyEvaluatorCategory as LegacyEvaluatorCategory, +) +from uipath_eval.models.models import ( + LegacyEvaluatorType as LegacyEvaluatorType, +) +from uipath_eval.models.models import ( + LLMResponse as LLMResponse, +) +from uipath_eval.models.models import ( + NumericEvaluationResult as NumericEvaluationResult, +) +from uipath_eval.models.models import ( + ScoreType as ScoreType, +) +from uipath_eval.models.models import ( + ToolCall as ToolCall, +) +from uipath_eval.models.models import ( + ToolOutput as ToolOutput, +) +from uipath_eval.models.models import ( + TrajectoryEvaluationSpan as TrajectoryEvaluationSpan, +) +from uipath_eval.models.models import ( + TrajectoryEvaluationTrace as TrajectoryEvaluationTrace, +) +from uipath_eval.models.models import ( + UiPathEvaluationError as UiPathEvaluationError, +) +from uipath_eval.models.models import ( + UiPathEvaluationErrorCategory as UiPathEvaluationErrorCategory, +) +from uipath_eval.models.models import ( + UiPathEvaluationErrorContract as UiPathEvaluationErrorContract, +) + +__all__ = [ + "AgentExecution", + "BaseEvaluationResult", + "BooleanEvaluationResult", + "ErrorEvaluationResult", + "EvalItemResult", + "EvaluationResult", + "EvaluationResultDto", + "EvaluatorType", + "LegacyEvaluatorCategory", + "LegacyEvaluatorType", + "LLMResponse", + "NumericEvaluationResult", + "ScoreType", + "ToolCall", + "ToolOutput", + "TrajectoryEvaluationSpan", + "TrajectoryEvaluationTrace", + "UiPathEvaluationError", + "UiPathEvaluationErrorCategory", + "UiPathEvaluationErrorContract", ] - - -class EvaluationResultDto(BaseModel): - """Serializable evaluation result used for aggregation and transport.""" - - model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) - - score: float - details: str | dict[str, Any] | None = None - evaluation_time: float | None = None - - @model_serializer(mode="wrap") - def serialize_model( - self, - serializer: core_schema.SerializerFunctionWrapHandler, - info: core_schema.SerializationInfo, - ) -> Any: - """Omit 'details' key from serialized output when it is None.""" - data = serializer(self) - if self.details is None and isinstance(data, dict): - data.pop("details", None) - return data - - @classmethod - def from_evaluation_result( - cls, evaluation_result: EvaluationResult - ) -> "EvaluationResultDto": - """Convert an EvaluationResult to a serializable DTO.""" - score_type = evaluation_result.score_type - score: float - if score_type == ScoreType.BOOLEAN: - score = 100 if evaluation_result.score else 0 - elif score_type == ScoreType.ERROR: - score = 0 - else: - score = evaluation_result.score - - # Convert BaseModel details to dict so Pydantic doesn't lose subclass fields - if isinstance(evaluation_result.details, BaseModel): - details: str | dict[str, Any] | None = ( - evaluation_result.details.model_dump() - ) - else: - details = evaluation_result.details - - return cls( - score=score, - details=details, - evaluation_time=evaluation_result.evaluation_time, - ) - - -class EvalItemResult(BaseModel): - """Result of a single evaluation item.""" - - evaluator_id: str - result: EvaluationResult - - -class LegacyEvaluatorCategory(IntEnum): - """Types of evaluators.""" - - Deterministic = 0 - LlmAsAJudge = 1 - AgentScorer = 2 - Trajectory = 3 - - @classmethod - def from_int(cls, value: int) -> "LegacyEvaluatorCategory": - """Construct EvaluatorCategory from an int value.""" - if value in cls._value2member_map_: - return cls(value) - else: - raise ValueError(f"{value} is not a valid EvaluatorCategory value") - - -class LegacyEvaluatorType(IntEnum): - """Subtypes of evaluators.""" - - Unknown = 0 - Equals = 1 - Contains = 2 - Regex = 3 - Factuality = 4 - Custom = 5 - JsonSimilarity = 6 - Trajectory = 7 - ContextPrecision = 8 - Faithfulness = 9 - CSVColumnExactMatch = 10 - - @classmethod - def from_int(cls, value: int) -> "LegacyEvaluatorType": - """Construct EvaluatorCategory from an int value.""" - if value in cls._value2member_map_: - return cls(value) - else: - raise ValueError(f"{value} is not a valid EvaluatorType value") - - -@dataclass -class TrajectoryEvaluationSpan: - """Simplified span representation for trajectory evaluation. - - Contains span information needed for evaluating agent execution paths, - excluding timestamps which are not useful for trajectory analysis. - """ - - name: str - status: str - attributes: dict[str, Any] - parent_name: str | None = None - events: list[dict[str, Any]] | None = None - - def __post_init__(self): - """Initialize default values.""" - if self.events is None: - self.events = [] - - @classmethod - def from_readable_span( - cls, span: ReadableSpan, parent_spans: dict[int, str] | None = None - ) -> "TrajectoryEvaluationSpan": - """Convert a ReadableSpan to a TrajectoryEvaluationSpan. - - Args: - span: The OpenTelemetry ReadableSpan to convert - parent_spans: Optional mapping of span IDs to names for parent lookup - - Returns: - TrajectoryEvaluationSpan with relevant data extracted - """ - # Extract status - status_map = {0: "unset", 1: "ok", 2: "error"} - status = status_map.get(span.status.status_code.value, "unknown") - - # Extract attributes - keep all attributes for now - attributes = {} - if span.attributes: - attributes = dict(span.attributes) - - # Get parent name if available - parent_name = None - if span.parent and parent_spans and span.parent.span_id in parent_spans: - parent_name = parent_spans[span.parent.span_id] - - # Extract events (without timestamps) - events = [] - if hasattr(span, "events") and span.events: - for event in span.events: - event_data = { - "name": event.name, - "attributes": dict(event.attributes) if event.attributes else {}, - } - events.append(event_data) - - return cls( - name=span.name, - status=status, - attributes=attributes, - parent_name=parent_name, - events=events, - ) - - def to_dict(self) -> dict[str, Any]: - """Convert to dictionary for JSON serialization.""" - return { - "name": self.name, - "status": self.status, - "parent_name": self.parent_name, - "attributes": self.attributes, - "events": self.events, - } - - -class TrajectoryEvaluationTrace(BaseModel): - """Container for a collection of trajectory evaluation spans.""" - - spans: list[TrajectoryEvaluationSpan] - - @classmethod - def from_readable_spans( - cls, spans: list[ReadableSpan] - ) -> "TrajectoryEvaluationTrace": - """Convert a list of ReadableSpans to TrajectoryEvaluationTrace. - - Args: - spans: List of OpenTelemetry ReadableSpans to convert - - Returns: - TrajectoryEvaluationTrace with converted spans - """ - # Create a mapping of span IDs to names for parent lookup - span_id_to_name = { - span.get_span_context().span_id: span.name # pyright: ignore[reportOptionalMemberAccess] - for span in spans - if span.get_span_context() is not None - } - - evaluation_spans = [ - TrajectoryEvaluationSpan.from_readable_span(span, span_id_to_name) - for span in spans - ] - - return cls(spans=evaluation_spans) - - model_config = ConfigDict(arbitrary_types_allowed=True) - - -class EvaluatorType(str, Enum): - """Evaluator type.""" - - CONTAINS = "uipath-contains" - EXACT_MATCH = "uipath-exact-match" - JSON_SIMILARITY = "uipath-json-similarity" - LLM_JUDGE_OUTPUT_SEMANTIC_SIMILARITY = "uipath-llm-judge-output-semantic-similarity" - LLM_JUDGE_OUTPUT_STRICT_JSON_SIMILARITY = ( - "uipath-llm-judge-output-strict-json-similarity" - ) - LLM_JUDGE_TRAJECTORY_SIMILARITY = "uipath-llm-judge-trajectory-similarity" - LLM_JUDGE_TRAJECTORY_SIMULATION = "uipath-llm-judge-trajectory-simulation" - LLM_JUDGE_TRAJECTORY = "uipath-llm-judge-trajectory" - LLM_JUDGE_OUTPUT = "uipath-llm-judge-output" - TOOL_CALL_ARGS = "uipath-tool-call-args" - TOOL_CALL_COUNT = "uipath-tool-call-count" - TOOL_CALL_ORDER = "uipath-tool-call-order" - TOOL_CALL_OUTPUT = "uipath-tool-call-output" - BINARY_CLASSIFICATION = "uipath-binary-classification" - MULTICLASS_CLASSIFICATION = "uipath-multiclass-classification" - - -class ToolCall(BaseModel): - """Represents a tool call with its arguments.""" - - name: str - args: dict[str, Any] - - -class ToolOutput(BaseModel): - """Represents a tool output with its output.""" - - name: str - output: str - - -class UiPathEvaluationErrorCategory(str, Enum): - """Categories of evaluation errors.""" - - SYSTEM = "System" - USER = "User" - UNKNOWN = "Unknown" - - -class UiPathEvaluationErrorContract(BaseModel): - """Standard error contract used across the runtime.""" - - code: str # Human-readable code uniquely identifying this error type across the platform. - # Format: . (e.g. LangGraph.InvaliGraphReference) - # Only use alphanumeric characters [A-Za-z0-9] and periods. No whitespace allowed. - - title: str # Short, human-readable summary of the problem that should remain consistent - # across occurrences. - - detail: ( - str # Human-readable explanation specific to this occurrence of the problem. - ) - # May include context, recommended actions, or technical details like call stacks - # for technical users. - - category: UiPathEvaluationErrorCategory = UiPathEvaluationErrorCategory.UNKNOWN - - -class UiPathEvaluationError(Exception): - """Base exception class for UiPath evaluation errors with structured error information.""" - - def __init__( - self, - code: str, - title: str, - detail: str, - category: UiPathEvaluationErrorCategory = UiPathEvaluationErrorCategory.UNKNOWN, - prefix: str = "Python", - include_traceback: bool = True, - ): - """Initialize the UiPathEvaluationError.""" - # Get the current traceback as a string - if include_traceback: - tb = traceback.format_exc() - if ( - tb and tb.strip() != "NoneType: None" - ): # Ensure there's an actual traceback - detail = f"{detail}\n\n{tb}" - - self.error_info = UiPathEvaluationErrorContract( - code=f"{prefix}.{code}", - title=title, - detail=detail, - category=category, - ) - super().__init__(detail) - - @property - def as_dict(self) -> dict[str, Any]: - """Get the error information as a dictionary.""" - return self.error_info.model_dump() diff --git a/packages/uipath/tests/evaluators/test_evaluator_helpers.py b/packages/uipath/tests/evaluators/test_evaluator_helpers.py index 84eb1159f..3732d1a29 100644 --- a/packages/uipath/tests/evaluators/test_evaluator_helpers.py +++ b/packages/uipath/tests/evaluators/test_evaluator_helpers.py @@ -10,6 +10,7 @@ from typing import Any import pytest +from uipath_eval.models.models import ToolCall, ToolOutput from uipath.eval._helpers.evaluators_helpers import ( extract_tool_calls, @@ -20,7 +21,6 @@ tool_calls_order_score, tool_calls_output_score, ) -from uipath.eval.models.models import ToolCall, ToolOutput class TestToolCallsOrderScore: diff --git a/packages/uipath/tests/evaluators/test_evaluator_methods.py b/packages/uipath/tests/evaluators/test_evaluator_methods.py index 22cfc980e..c9d7018bf 100644 --- a/packages/uipath/tests/evaluators/test_evaluator_methods.py +++ b/packages/uipath/tests/evaluators/test_evaluator_methods.py @@ -16,6 +16,12 @@ import pytest from opentelemetry.sdk.trace import ReadableSpan from pytest_mock.plugin import MockerFixture +from uipath_eval.models.models import ( + AgentExecution, + ToolCall, + ToolOutput, + UiPathEvaluationError, +) from uipath.eval.evaluators.base_evaluator import BaseEvaluatorJustification from uipath.eval.evaluators.contains_evaluator import ( @@ -57,12 +63,6 @@ ToolCallOutputEvaluatorJustification, ) from uipath.eval.models import NumericEvaluationResult -from uipath.eval.models.models import ( - AgentExecution, - ToolCall, - ToolOutput, - UiPathEvaluationError, -) @pytest.fixture diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 22bd59925..59c05fa79 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.54" +version = "2.10.55" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2563,6 +2563,7 @@ dependencies = [ { name = "tenacity" }, { name = "truststore" }, { name = "uipath-core" }, + { name = "uipath-eval" }, { name = "uipath-platform" }, { name = "uipath-runtime" }, ] @@ -2615,6 +2616,7 @@ requires-dist = [ { name = "tenacity", specifier = ">=9.0.0" }, { name = "truststore", specifier = ">=0.10.1" }, { name = "uipath-core", editable = "../uipath-core" }, + { name = "uipath-eval", editable = "../uipath-eval" }, { name = "uipath-platform", editable = "../uipath-platform" }, { name = "uipath-runtime", specifier = ">=0.10.1,<0.11.0" }, ] @@ -2680,6 +2682,42 @@ dev = [ { name = "rust-just", specifier = ">=1.39.0" }, ] +[[package]] +name = "uipath-eval" +version = "0.1.0" +source = { editable = "../uipath-eval" } +dependencies = [ + { name = "httpx" }, + { name = "opentelemetry-sdk" }, + { name = "pydantic" }, + { name = "uipath-core" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "langchain-core", marker = "extra == 'llm'", specifier = ">=0.3" }, + { name = "openai", marker = "extra == 'llm'", specifier = ">=1.0" }, + { name = "opentelemetry-sdk", specifier = ">=1.39.0,<2.0.0" }, + { name = "pydantic", specifier = ">=2.12.5,<3.0.0" }, + { name = "uipath-core", specifier = ">=0.5.8,<0.6.0" }, +] +provides-extras = ["llm"] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit", specifier = ">=1.8.2" }, + { name = "mypy", specifier = ">=1.14.1" }, + { name = "pre-commit", specifier = ">=4.5.1" }, + { name = "pytest", specifier = ">=7.4.0" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "pytest-cov", specifier = ">=4.1.0" }, + { name = "pytest-httpx", specifier = ">=0.35.0" }, + { name = "pytest-mock", specifier = ">=3.11.1" }, + { name = "ruff", specifier = ">=0.9.4" }, + { name = "rust-just", specifier = ">=1.39.0" }, +] + [[package]] name = "uipath-platform" version = "0.1.35"