From 4175247134ae4fdb0bf4fabf9c427b2f0e92c193 Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Mon, 15 Jun 2026 16:13:58 -0300 Subject: [PATCH 1/9] fix(tests): install azure-mgmt-authorization in dev to unblock Release (#311) The unit test `test_list_role_definition_ids_extracts_guid_suffix` patches `azure.mgmt.authorization.AuthorizationManagementClient` via `unittest.mock.patch` (string form), which requires the package to be importable in the test environment. The `Release` workflow's reusable `_build.yml` and `ci.yml` both run `uv sync --group dev`, but `azure-mgmt-authorization` was only declared under the `agent` optional-dependencies extra, not under the `dev` group. CI therefore hit `AttributeError: module 'azure' has no attribute 'mgmt'` (release build, where `azure.mgmt` namespace is partially populated by other `azure-mgmt-*` deps) or `ModuleNotFoundError: No module named 'azure.mgmt'` (PR CI, where no `azure-mgmt-*` is installed at all), breaking the `Release` workflow for v0.4.0 and v0.4.1. Adding `azure-mgmt-authorization` to the `dev` group makes the package available wherever the test suite runs, mirroring the `agent` extra where it is already a runtime dependency. No change to `src/` runtime code. `uv lock` also picks up develop's already-merged dependabot bumps (`azure-mgmt-cognitiveservices<15`, `azure-monitor-query<3`, `pandas<4`). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pyproject.toml | 1 + uv.lock | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7a74d519..60627a78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ where = ["src"] [dependency-groups] dev = [ "azure-ai-evaluation>=1.0,<2.0", + "azure-mgmt-authorization>=4.0,<5.0", "mypy>=1.19.1", "pre-commit>=4.0", "pytest>=8.0", diff --git a/uv.lock b/uv.lock index e016e652..fee103f7 100644 --- a/uv.lock +++ b/uv.lock @@ -21,6 +21,7 @@ dependencies = [ [package.optional-dependencies] agent = [ { name = "azure-identity" }, + { name = "azure-mgmt-authorization" }, { name = "azure-mgmt-cognitiveservices" }, { name = "azure-mgmt-monitor" }, { name = "azure-monitor-opentelemetry" }, @@ -44,6 +45,7 @@ mcp = [ [package.dev-dependencies] dev = [ { name = "azure-ai-evaluation" }, + { name = "azure-mgmt-authorization" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, @@ -58,17 +60,18 @@ requires-dist = [ { name = "azure-ai-projects", specifier = ">=2.0.1,<3.0" }, { name = "azure-identity", marker = "extra == 'agent'", specifier = ">=1.17,<2.0" }, { name = "azure-identity", marker = "extra == 'foundry'", specifier = ">=1.17,<2.0" }, - { name = "azure-mgmt-cognitiveservices", marker = "extra == 'agent'", specifier = ">=13.5,<14.0" }, + { name = "azure-mgmt-authorization", marker = "extra == 'agent'", specifier = ">=4.0,<5.0" }, + { name = "azure-mgmt-cognitiveservices", marker = "extra == 'agent'", specifier = ">=13.5,<15.0" }, { name = "azure-mgmt-monitor", marker = "extra == 'agent'", specifier = ">=6.0,<7.0" }, { name = "azure-monitor-opentelemetry", marker = "extra == 'agent'", specifier = ">=1.6,<2.0" }, { name = "azure-monitor-opentelemetry", marker = "extra == 'foundry'", specifier = ">=1.6,<2.0" }, - { name = "azure-monitor-query", marker = "extra == 'agent'", specifier = ">=1.3,<2.0" }, + { name = "azure-monitor-query", marker = "extra == 'agent'", specifier = ">=1.3,<3.0" }, { name = "cryptography", marker = "extra == 'agent'", specifier = ">=42" }, { name = "fastapi", marker = "extra == 'agent'", specifier = ">=0.110,<1.0" }, { name = "httpx", marker = "extra == 'agent'", specifier = ">=0.27,<1.0" }, { name = "markdown", marker = "extra == 'agent'", specifier = ">=3.6,<4.0" }, { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.0,<2" }, - { name = "pandas", marker = "extra == 'foundry'", specifier = ">=2.0,<3.0" }, + { name = "pandas", marker = "extra == 'foundry'", specifier = ">=2.0,<4.0" }, { name = "pydantic", specifier = ">=2,<3" }, { name = "ruamel-yaml", specifier = ">=0.18,<1.0" }, { name = "typer", specifier = ">=0.12,<1.0" }, @@ -79,6 +82,7 @@ provides-extras = ["mcp", "foundry", "agent"] [package.metadata.requires-dev] dev = [ { name = "azure-ai-evaluation", specifier = ">=1.0,<2.0" }, + { name = "azure-mgmt-authorization", specifier = ">=4.0,<5.0" }, { name = "mypy", specifier = ">=1.19.1" }, { name = "pre-commit", specifier = ">=4.0" }, { name = "pytest", specifier = ">=8.0" }, @@ -351,6 +355,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/9a/417b3a533e01953a7c618884df2cb05a71e7b68bdbce4fbdb62349d2a2e8/azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c", size = 192138, upload-time = "2026-03-13T01:12:22.951Z" }, ] +[[package]] +name = "azure-mgmt-authorization" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-mgmt-core" }, + { name = "isodate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ab/e79874f166eed24f4456ce4d532b29a926fb4c798c2c609eefd916a3f73d/azure-mgmt-authorization-4.0.0.zip", hash = "sha256:69b85abc09ae64fc72975bd43431170d8c7eb5d166754b98aac5f3845de57dc4", size = 1134795, upload-time = "2023-07-25T04:47:46.033Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/b3/8ec1268082f4d20cc8bf723a1a8e6b9e330bcc338a4dbcee9c7737e9dc1c/azure_mgmt_authorization-4.0.0-py3-none-any.whl", hash = "sha256:d8feeb3842e6ddf1a370963ca4f61fb6edc124e8997b807dd025bc9b2379cd1a", size = 1072620, upload-time = "2023-07-25T04:47:49.26Z" }, +] + [[package]] name = "azure-mgmt-cognitiveservices" version = "13.7.0" From ec11d714b485a919afd79c898d39a318cf142aed Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Wed, 17 Jun 2026 08:42:59 -0300 Subject: [PATCH 2/9] fix(eval): support azd ai agent eval generate with init fallback azure.ai.agents extension 0.1.40 renamed 'azd ai agent eval init' to 'azd ai agent eval generate', causing 'agentops eval init' to hard-fail on the deprecation error. run_azd_eval_init now tries 'generate' first and falls back to the legacy 'init' subcommand only when the installed extension reports the subcommand name as unknown/unrecognized/deprecated. Genuine failures (auth, endpoint, timeout) are surfaced immediately. All preserved flags and recipe discovery/persistence behaviour are unchanged. Adds _eval_subcommand_unsupported / _run_eval_subcommand helpers and unit tests covering the new-extension, old-extension fallback, deprecation, and real-error-passthrough paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 14 ++ src/agentops/services/azd_eval_init.py | 162 +++++++++++++++++----- tests/unit/test_azd_eval_init.py | 177 ++++++++++++++++++++++++- 3 files changed, 316 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 067f179e..2fc43b7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,20 @@ This format follows [Keep a Changelog](https://keepachangelog.com/) and adheres tutorial are updated to describe the new contract. ([#214](https://github.com/Azure/agentops/issues/214)) +### Fixed +- **`agentops eval init` now works with both old and new `azure.ai.agents` azd + extensions.** Version 0.1.40 of the extension renamed the eval subcommand from + `azd ai agent eval init` to `azd ai agent eval generate`, which made + `agentops eval init` hard-fail with `Command "init" is deprecated, use 'azd ai + agent eval generate' instead`. AgentOps now invokes `generate` first and + transparently falls back to the legacy `init` subcommand when an older + extension does not recognise `generate`. The fallback only triggers on + subcommand-name/deprecation errors; genuine failures (authentication, project + endpoint, timeouts) are still surfaced immediately and unchanged. All + previously passed flags (`--project-endpoint`, `--agent`, + `--gen-instruction-file`, `--eval-model`, `--dataset`, `--evaluator`) and the + recipe discovery/persistence behaviour are preserved. + ## [0.4.0] - 2026-06-14 ### Added diff --git a/src/agentops/services/azd_eval_init.py b/src/agentops/services/azd_eval_init.py index 2162769b..0c78b755 100644 --- a/src/agentops/services/azd_eval_init.py +++ b/src/agentops/services/azd_eval_init.py @@ -1,4 +1,10 @@ -"""Wrapper helpers for ``azd ai agent eval init``.""" +"""Wrapper helpers for ``azd ai agent eval`` recipe generation. + +The ``azure.ai.agents`` azd extension renamed this subcommand in 0.1.40: +``azd ai agent eval init`` became ``azd ai agent eval generate``. These helpers +prefer the new ``generate`` name and fall back to the legacy ``init`` name so +AgentOps keeps working across extension versions. +""" from __future__ import annotations @@ -62,9 +68,11 @@ def run_azd_eval_init( force: bool = False, timeout_seconds: float = AZD_EVAL_TIMEOUT_SECONDS, ) -> AzdEvalInitResult: - """Run ``azd ai agent eval init`` and persist ``eval_recipe``. + """Run ``azd ai agent eval generate`` and persist ``eval_recipe``. - The azd command remains the source of truth for generating datasets, + Prefers the ``generate`` subcommand (azure.ai.agents >= 0.1.40) and falls + back to the legacy ``init`` subcommand on older extensions. The azd command + remains the source of truth for generating datasets, evaluators, and rubric assets. AgentOps only delegates the command, finds the generated recipe, and records the recipe path in ``agentops.yaml`` so future gates are deterministic. @@ -95,17 +103,18 @@ def run_azd_eval_init( f"{AZD_EXTENSION_NAME}`), then rerun `agentops eval init`." ) - command = ["azd", "--no-prompt", "ai", "agent", "eval", "init"] + base_command = ["azd", "--no-prompt", "ai", "agent", "eval"] + arguments: list[str] = [] project_endpoint = _project_endpoint_from_config_or_env(resolved_config) if project_endpoint: - command.extend(["--project-endpoint", project_endpoint]) + arguments.extend(["--project-endpoint", project_endpoint]) agent_name = _agent_name_from_config(resolved_config) if agent_name: - command.extend(["--agent", agent_name]) + arguments.extend(["--agent", agent_name]) effective_dataset = dataset or _dataset_from_config(resolved_config) instruction_file = _prompt_file_from_config(resolved_config) if effective_dataset is None and instruction_file is not None: - command.extend( + arguments.extend( [ "--gen-instruction-file", _command_path(instruction_file, workspace=root), @@ -113,47 +122,29 @@ def run_azd_eval_init( ) eval_model = _eval_model_from_config(resolved_config) if eval_model: - command.extend(["--eval-model", eval_model]) + arguments.extend(["--eval-model", eval_model]) if effective_dataset is not None: effective_dataset = _azd_dataset_from_agentops_dataset( effective_dataset, workspace=root, ) - command.extend( + arguments.extend( ["--dataset", _command_path(effective_dataset, workspace=root)] ) for evaluator in _azd_evaluators_from_config(resolved_config): - command.extend(["--evaluator", evaluator]) - - try: - completed = subprocess.run( - command, - cwd=str(root), - text=True, - encoding="utf-8", - errors="replace", - capture_output=True, - timeout=timeout_seconds, - check=False, - ) - except FileNotFoundError as exc: - raise AzdBackendError( - "azd was not found on PATH. Install the Azure Developer CLI and the " - f"`{AZD_EXTENSION_NAME}` extension, then rerun `agentops eval init`." - ) from exc - except subprocess.TimeoutExpired as exc: - raise AzdBackendError( - f"{' '.join(command)} timed out after {timeout_seconds:g}s." - ) from exc + arguments.extend(["--evaluator", evaluator]) - if completed.returncode != 0: - detail = completed.stderr.strip() or completed.stdout.strip() or f"exit code {completed.returncode}" - raise AzdBackendError(f"azd ai agent eval init failed: {detail}") + completed = _run_eval_subcommand( + base_command, + arguments, + cwd=root, + timeout_seconds=timeout_seconds, + ) recipe = find_eval_yaml(root) if recipe is None: raise AzdBackendError( - "azd ai agent eval init completed, but AgentOps could not find the " + "azd ai agent eval completed, but AgentOps could not find the " "generated eval.yaml. Move it under the workspace root or src// " "and set `eval_recipe:` in agentops.yaml." ) @@ -175,6 +166,107 @@ def _find_recipe_if_unambiguous(workspace: Path) -> Optional[Path]: return None +# azd renamed this subcommand in the ``azure.ai.agents`` extension 0.1.40: +# ``init`` became ``generate``. Try the new name first and fall back to the +# legacy name so AgentOps works whether the consumer has an old or new +# extension installed. +_EVAL_SUBCOMMANDS: tuple[str, ...] = ("generate", "init") + + +def _eval_subcommand_unsupported(*outputs: str) -> bool: + """Return True when azd reports the eval subcommand name is unknown/deprecated. + + Matches the azd/cobra-style messages emitted when an installed + ``azure.ai.agents`` extension does not recognise a subcommand name (older + extensions lack ``generate``) or reports the legacy ``init`` name as + deprecated (newer extensions). Centralised here so the fallback decision is + unit-testable and robust to minor wording changes. + """ + haystack = " ".join(text.lower() for text in outputs if text) + return any( + phrase in haystack + for phrase in ( + "unknown command", + "unrecognized", + "is not a valid", + "invalid command", + "is deprecated, use", + ) + ) + + +def _azd_failure_detail(completed: "subprocess.CompletedProcess[str]") -> str: + return ( + completed.stderr.strip() + or completed.stdout.strip() + or f"exit code {completed.returncode}" + ) + + +def _run_eval_subcommand( + base_command: list[str], + arguments: list[str], + *, + cwd: Path, + timeout_seconds: float, +) -> "subprocess.CompletedProcess[str]": + """Run ``azd ai agent eval `` resiliently across extensions. + + Prefers ``generate`` (azure.ai.agents >= 0.1.40) and falls back to the + legacy ``init`` subcommand when the installed extension does not recognise + ``generate``. A non-zero result that is not a subcommand-name problem (for + example an authentication or endpoint error) is surfaced immediately rather + than masked by the fallback, preserving the previous error behaviour. + """ + last_completed: Optional["subprocess.CompletedProcess[str]"] = None + for subcommand in _EVAL_SUBCOMMANDS: + command = [*base_command, subcommand, *arguments] + try: + completed = subprocess.run( + command, + cwd=str(cwd), + text=True, + encoding="utf-8", + errors="replace", + capture_output=True, + timeout=timeout_seconds, + check=False, + ) + except FileNotFoundError as exc: + raise AzdBackendError( + "azd was not found on PATH. Install the Azure Developer CLI and " + f"the `{AZD_EXTENSION_NAME}` extension, then rerun `agentops eval " + "init`." + ) from exc + except subprocess.TimeoutExpired as exc: + raise AzdBackendError( + f"{' '.join(command)} timed out after {timeout_seconds:g}s." + ) from exc + + if completed.returncode == 0: + return completed + + if _eval_subcommand_unsupported(completed.stderr, completed.stdout): + # This subcommand name is not supported (or is deprecated) by the + # installed extension. Remember it and try the next candidate. + last_completed = completed + continue + + # A real failure (not a subcommand-name issue): surface it now. + raise AzdBackendError( + f"azd ai agent eval {subcommand} failed: {_azd_failure_detail(completed)}" + ) + + if last_completed is not None: + detail = _azd_failure_detail(last_completed) + else: # pragma: no cover - _EVAL_SUBCOMMANDS is never empty + detail = ( + "no azd eval subcommand (generate/init) was accepted by the " + f"installed `{AZD_EXTENSION_NAME}` extension" + ) + raise AzdBackendError(f"azd ai agent eval failed: {detail}") + + def _dataset_from_config(config_path: Path) -> Optional[Path]: data = load_yaml(config_path) raw_dataset = data.get("dataset") diff --git a/tests/unit/test_azd_eval_init.py b/tests/unit/test_azd_eval_init.py index 29240b6a..d21123be 100644 --- a/tests/unit/test_azd_eval_init.py +++ b/tests/unit/test_azd_eval_init.py @@ -67,7 +67,7 @@ def fake_run(command, **kwargs): "ai", "agent", "eval", - "init", + "generate", "--project-endpoint", "https://contoso.services.ai.azure.com/api/projects/travel", "--agent", @@ -133,7 +133,7 @@ def fake_run(command, **kwargs): "ai", "agent", "eval", - "init", + "generate", "--project-endpoint", "https://contoso.services.ai.azure.com/api/projects/travel", "--agent", @@ -349,3 +349,176 @@ def fake_init(**kwargs): assert result.exit_code == 0 assert " * updated" in result.output assert "✓" not in result.output + + +def _setup_eval_workspace(tmp_path: Path) -> Path: + """Create a minimal workspace and return the config path.""" + config_path = tmp_path / "agentops.yaml" + _write_config(config_path) + dataset = tmp_path / ".agentops" / "data" / "smoke.jsonl" + dataset.parent.mkdir(parents=True) + dataset.write_text('{"input":"hello"}\n', encoding="utf-8") + prompt_file = tmp_path / ".agentops" / "prompts" / "travel.md" + prompt_file.parent.mkdir(parents=True) + prompt_file.write_text("You are a travel planner.", encoding="utf-8") + return config_path + + +def _eval_subcommand(command: list[str]) -> str: + # command is ["azd", "--no-prompt", "ai", "agent", "eval", , ...] + return command[5] + + +def test_run_azd_eval_init_prefers_generate_on_new_extension( + tmp_path: Path, + monkeypatch, +) -> None: + config_path = _setup_eval_workspace(tmp_path) + monkeypatch.setattr(azd_eval_init, "azd_available", lambda *, cwd=None: True) + + subcommands: list[str] = [] + + def fake_run(command, **kwargs): + if command[:3] == ["az", "resource", "list"]: + return subprocess.CompletedProcess(command, 0, stdout="[]", stderr="") + subcommands.append(_eval_subcommand(command)) + recipe = Path(kwargs["cwd"]) / "eval.yaml" + recipe.write_text("name: travel-agent-eval\n", encoding="utf-8") + return subprocess.CompletedProcess(command, 0, stdout="created", stderr="") + + monkeypatch.setattr(subprocess, "run", fake_run) + + result = azd_eval_init.run_azd_eval_init( + workspace=tmp_path, + config_path=config_path, + ) + + assert result.command_ran is True + # generate succeeds on the first try; the legacy init is never invoked. + assert subcommands == ["generate"] + + +def test_run_azd_eval_init_falls_back_to_init_on_old_extension( + tmp_path: Path, + monkeypatch, +) -> None: + config_path = _setup_eval_workspace(tmp_path) + monkeypatch.setattr(azd_eval_init, "azd_available", lambda *, cwd=None: True) + + seen: list[list[str]] = [] + + def fake_run(command, **kwargs): + if command[:3] == ["az", "resource", "list"]: + return subprocess.CompletedProcess(command, 0, stdout="[]", stderr="") + seen.append(command) + subcommand = _eval_subcommand(command) + if subcommand == "generate": + # Older azure.ai.agents extensions do not know `generate`. + return subprocess.CompletedProcess( + command, + 1, + stdout="", + stderr='Error: unknown command "generate" for "azd ai agent eval"', + ) + recipe = Path(kwargs["cwd"]) / "eval.yaml" + recipe.write_text("name: travel-agent-eval\n", encoding="utf-8") + return subprocess.CompletedProcess(command, 0, stdout="created", stderr="") + + monkeypatch.setattr(subprocess, "run", fake_run) + + result = azd_eval_init.run_azd_eval_init( + workspace=tmp_path, + config_path=config_path, + ) + + assert result.command_ran is True + eval_calls = [ + cmd for cmd in seen if cmd[:5] == ["azd", "--no-prompt", "ai", "agent", "eval"] + ] + assert [_eval_subcommand(cmd) for cmd in eval_calls] == ["generate", "init"] + # The preserved flags are identical for both attempts (only the subcommand + # token differs). + assert eval_calls[0][6:] == eval_calls[1][6:] + updated = config_path.read_text(encoding="utf-8") + assert "eval_recipe:" in updated + + +def test_run_azd_eval_init_init_deprecation_does_not_hard_fail( + tmp_path: Path, + monkeypatch, +) -> None: + # On a new extension, init is deprecated but generate works. The wrapper + # must succeed via generate and never surface the deprecation as an error. + config_path = _setup_eval_workspace(tmp_path) + monkeypatch.setattr(azd_eval_init, "azd_available", lambda *, cwd=None: True) + + def fake_run(command, **kwargs): + if command[:3] == ["az", "resource", "list"]: + return subprocess.CompletedProcess(command, 0, stdout="[]", stderr="") + if _eval_subcommand(command) == "init": + raise AssertionError("init must not be invoked when generate works") + recipe = Path(kwargs["cwd"]) / "eval.yaml" + recipe.write_text("name: travel-agent-eval\n", encoding="utf-8") + return subprocess.CompletedProcess(command, 0, stdout="created", stderr="") + + monkeypatch.setattr(subprocess, "run", fake_run) + + result = azd_eval_init.run_azd_eval_init( + workspace=tmp_path, + config_path=config_path, + ) + + assert result.command_ran is True + + +def test_run_azd_eval_init_surfaces_real_errors_without_fallback( + tmp_path: Path, + monkeypatch, +) -> None: + config_path = _setup_eval_workspace(tmp_path) + monkeypatch.setattr(azd_eval_init, "azd_available", lambda *, cwd=None: True) + + subcommands: list[str] = [] + + def fake_run(command, **kwargs): + if command[:3] == ["az", "resource", "list"]: + return subprocess.CompletedProcess(command, 0, stdout="[]", stderr="") + subcommands.append(_eval_subcommand(command)) + return subprocess.CompletedProcess( + command, + 1, + stdout="", + stderr="ERROR: failed to authenticate to the Azure AI project", + ) + + monkeypatch.setattr(subprocess, "run", fake_run) + + try: + azd_eval_init.run_azd_eval_init( + workspace=tmp_path, + config_path=config_path, + ) + except azd_eval_init.AzdBackendError as exc: + assert "failed to authenticate" in str(exc) + assert "azd ai agent eval generate failed" in str(exc) + else: # pragma: no cover - assertion helper + raise AssertionError("expected AzdBackendError") + + # A genuine error must not trigger the init fallback. + assert subcommands == ["generate"] + + +def test_eval_subcommand_unsupported_matches_known_messages() -> None: + assert azd_eval_init._eval_subcommand_unsupported( + 'Error: unknown command "generate" for "azd ai agent eval"' + ) + assert azd_eval_init._eval_subcommand_unsupported( + "Command \"init\" is deprecated, use 'azd ai agent eval generate' instead" + ) + assert azd_eval_init._eval_subcommand_unsupported("Unrecognized flag") + assert azd_eval_init._eval_subcommand_unsupported('"generate" is not a valid command') + # Real runtime errors must not be treated as a subcommand-name problem. + assert not azd_eval_init._eval_subcommand_unsupported( + "ERROR: failed to authenticate to the Azure AI project" + ) + assert not azd_eval_init._eval_subcommand_unsupported("", "") From ae482d0ee88284d3e34038b32313ee6685b8710e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:43:24 +0000 Subject: [PATCH 3/9] chore(deps): bump actions/checkout from 6 to 7 Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/_build.yml | 2 +- .github/workflows/agentops-watchdog.yml | 2 +- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/cut-release.yml | 2 +- .github/workflows/e2e.yml | 18 +++++++++--------- .github/workflows/release.yml | 6 +++--- .github/workflows/staging.yml | 4 ++-- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index 80f5009d..508b17bc 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -39,7 +39,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ inputs.checkout_ref || github.ref }} fetch-depth: 0 # Full history required for setuptools-scm diff --git a/.github/workflows/agentops-watchdog.yml b/.github/workflows/agentops-watchdog.yml index f87eccd6..637f0a46 100644 --- a/.github/workflows/agentops-watchdog.yml +++ b/.github/workflows/agentops-watchdog.yml @@ -44,7 +44,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Azure login (OIDC) uses: azure/login@v3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4529df3..d07e3a23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v7 @@ -66,7 +66,7 @@ jobs: os: [ubuntu-latest, windows-latest] python-version: ["3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v7 @@ -93,7 +93,7 @@ jobs: runs-on: ubuntu-latest needs: test steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v7 @@ -125,7 +125,7 @@ jobs: permissions: id-token: write # Required for PyPI Trusted Publishing (OIDC) steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 # Full history for setuptools-scm @@ -162,7 +162,7 @@ jobs: needs: publish-dev runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 @@ -215,7 +215,7 @@ jobs: build-vsix: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 # CI uses the committed package.json version as-is (no publish, dry-run only). # The version in package.json is synced by cut-release.yml when a release branch is created. diff --git a/.github/workflows/cut-release.yml b/.github/workflows/cut-release.yml index 51d4d756..aa3d43da 100644 --- a/.github/workflows/cut-release.yml +++ b/.github/workflows/cut-release.yml @@ -48,7 +48,7 @@ jobs: echo "version=$VERSION" >> "$GITHUB_ENV" - name: Checkout develop - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: ref: develop fetch-depth: 0 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7aefdab6..2c3c3062 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -36,7 +36,7 @@ jobs: offline-smoke: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v6 @@ -69,7 +69,7 @@ jobs: unit-tests-with-coverage: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up Python uses: actions/setup-python@v6 @@ -127,7 +127,7 @@ jobs: hosted_agent_name: ${{ steps.create_hosted_agent.outputs.agent_name }} suffix: ${{ steps.suffix.outputs.value }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - id: suffix name: Compute per-run suffix @@ -256,7 +256,7 @@ jobs: id-token: write contents: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: "3.12" @@ -307,7 +307,7 @@ jobs: id-token: write contents: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: "3.12" @@ -359,7 +359,7 @@ jobs: id-token: write contents: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: "3.12" @@ -409,7 +409,7 @@ jobs: id-token: write contents: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: "3.12" @@ -464,7 +464,7 @@ jobs: id-token: write contents: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Azure login (OIDC) uses: ./.github/actions/azure-oidc-login with: @@ -529,7 +529,7 @@ jobs: if: always() runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Download all artifacts uses: actions/download-artifact@v8 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99d1ad9b..c45e49f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,7 +98,7 @@ jobs: needs: publish-testpypi runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 @@ -185,7 +185,7 @@ jobs: env: VSIX_FILE: agentops-skills.vsix steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 # Full history for version derivation @@ -255,7 +255,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Download Python dist artifacts uses: actions/download-artifact@v8 diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 59cf2d79..a39d2870 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -79,7 +79,7 @@ jobs: needs: publish-testpypi runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 @@ -137,7 +137,7 @@ jobs: runs-on: ubuntu-latest environment: staging steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Sync VSIX version from branch name run: | From 8fd3f3925b279be42d0b8f1c5e6169310ece4ecb Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Fri, 26 Jun 2026 10:15:40 -0300 Subject: [PATCH 4/9] fix: declare click runtime dependency Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 +++++ pyproject.toml | 1 + uv.lock | 2 ++ 3 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc43b7d..d1de82bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,11 @@ This format follows [Keep a Changelog](https://keepachangelog.com/) and adheres ([#214](https://github.com/Azure/agentops/issues/214)) ### Fixed +- **Clean installs now include the pager dependency used by explain commands.** + `agentops explain`, `agentops init explain`, and `agentops doctor explain` + import Click directly to render long manual output, so `click>=8.1,<9` is now + declared as a runtime dependency instead of relying on transitive installs. + - **`agentops eval init` now works with both old and new `azure.ai.agents` azd extensions.** Version 0.1.40 of the extension renamed the eval subcommand from `azd ai agent eval init` to `azd ai agent eval generate`, which made diff --git a/pyproject.toml b/pyproject.toml index 60627a78..674e8576 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "typer>=0.12,<1.0", + "click>=8.1,<9", "pydantic>=2,<3", "ruamel.yaml>=0.18,<1.0", "azure-ai-projects>=2.0.1,<3.0", diff --git a/uv.lock b/uv.lock index fee103f7..242b1c55 100644 --- a/uv.lock +++ b/uv.lock @@ -13,6 +13,7 @@ name = "agentops-accelerator" source = { editable = "." } dependencies = [ { name = "azure-ai-projects" }, + { name = "click" }, { name = "pydantic" }, { name = "ruamel-yaml" }, { name = "typer" }, @@ -66,6 +67,7 @@ requires-dist = [ { name = "azure-monitor-opentelemetry", marker = "extra == 'agent'", specifier = ">=1.6,<2.0" }, { name = "azure-monitor-opentelemetry", marker = "extra == 'foundry'", specifier = ">=1.6,<2.0" }, { name = "azure-monitor-query", marker = "extra == 'agent'", specifier = ">=1.3,<3.0" }, + { name = "click", specifier = ">=8.1,<9" }, { name = "cryptography", marker = "extra == 'agent'", specifier = ">=42" }, { name = "fastapi", marker = "extra == 'agent'", specifier = ">=0.110,<1.0" }, { name = "httpx", marker = "extra == 'agent'", specifier = ">=0.27,<1.0" }, From d0fe89f5456a7d0461eb5770799657b5c38bdae7 Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Fri, 26 Jun 2026 11:02:31 -0300 Subject: [PATCH 5/9] chore(deps): refresh Python dependency lock Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- uv.lock | 324 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 170 insertions(+), 154 deletions(-) diff --git a/uv.lock b/uv.lock index 242b1c55..ab46980c 100644 --- a/uv.lock +++ b/uv.lock @@ -104,7 +104,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.5" +version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -113,95 +113,111 @@ dependencies = [ { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, - { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, - { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, - { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, - { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, - { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, - { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, - { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, - { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, - { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, - { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, - { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, - { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, - { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, - { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, - { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, - { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, - { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, - { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, - { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, - { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, - { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/dd/bf526e6f0a1120dd6f2df2e97bacfe4d358f13d17a0ff5847301a1375a51/aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2", size = 765225, upload-time = "2026-06-07T21:06:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e1/a2872aa55495a70f61310d411541c6ee23812d9a884e000c716e1bc3edbf/aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f", size = 518743, upload-time = "2026-06-07T21:06:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e7/c60c7b209e509cc787de3cea0550a518538cfc08003e1c1e14c1c63fff71/aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8", size = 514139, upload-time = "2026-06-07T21:06:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8d/614ace2f579702c9840ab1e1447fd8509e35b0b904f7196418fa2f57b25d/aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04", size = 1784088, upload-time = "2026-06-07T21:06:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/726e90f99542bf292f81a96a12cc4847deb86f3ccf62c6f4014a201f4d33/aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8", size = 1737835, upload-time = "2026-06-07T21:06:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4b/d176d5c4db9d33dacf0543102ea59503bc1d528af4cfd0b719949ca49389/aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6", size = 1842801, upload-time = "2026-06-07T21:06:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d6/5a99b563690ea0cbed912ae94a2ce33993a5709a651a3a4fe761e7dd973a/aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af", size = 1929992, upload-time = "2026-06-07T21:06:17.947Z" }, + { url = "https://files.pythonhosted.org/packages/76/7f/a987b14a3859094b3cea3f4825219c3e5536242564af6e3f9c2f6c994eb2/aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730", size = 1786989, upload-time = "2026-06-07T21:06:19.677Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1a/420e5c85a3e73349372ed22ce0b6af86bfa6ce16a4b20a64a2e94608c781/aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621", size = 1640129, upload-time = "2026-06-07T21:06:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/a7/80/18a592ed3be0a402cc03670bd72ee1f8563ddbe1d8d5542dbf868f274136/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee", size = 1756576, upload-time = "2026-06-07T21:06:24.8Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0b/8b3d5713373858ff71a617daf6e3b0e81ad63e79d09a3cf2f6b6b983939c/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573", size = 1754668, upload-time = "2026-06-07T21:06:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/9f/49/fd564575cf225821d7ba5a117cb8bc27213d8a7e1811162afb43ae077039/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7", size = 1817019, upload-time = "2026-06-07T21:06:28.297Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/e850c9ae6fc91356552ae668bb6c51e93fa29c8aef13398a10b56678557f/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf", size = 1631638, upload-time = "2026-06-07T21:06:30.242Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/3c337ba72451a89806ace6f75bddc92bafc5b8d53d90115a512858024b63/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85", size = 1835660, upload-time = "2026-06-07T21:06:31.943Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9c/9c18cf367a0498212d9ba7daf990b504a5e8ae064cda4b504e2647c89c03/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3", size = 1775698, upload-time = "2026-06-07T21:06:33.72Z" }, + { url = "https://files.pythonhosted.org/packages/b5/63/a251a9d2a6cb45065b2ddc0bde2b3dd10108740a9a42f632c66405a761a2/aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126", size = 458386, upload-time = "2026-06-07T21:06:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/17/ca/69274c51dcd6e8947d77b2806cf47a4a15f2c846e2cbeb1882547d3da283/aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5", size = 483406, upload-time = "2026-06-07T21:06:36.824Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/c25904f77690c3688ec140f87591ef11a0cfe36bf3d5c0f1f38056fb62b3/aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b", size = 452987, upload-time = "2026-06-07T21:06:38.371Z" }, + { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" }, + { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" }, + { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" }, + { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" }, + { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" }, + { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" }, + { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" }, + { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, + { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, + { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, + { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, + { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, + { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, + { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, + { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, + { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, + { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, + { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, + { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, + { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, + { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, + { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, + { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, ] [[package]] @@ -786,61 +802,61 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.7" +version = "48.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, - { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, - { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, - { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, - { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, - { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, - { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, - { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, - { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, - { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, - { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, - { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, - { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, - { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, - { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, - { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" }, + { url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" }, + { url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" }, + { url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" }, + { url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" }, + { url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" }, + { url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" }, + { url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" }, + { url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" }, + { url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" }, + { url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" }, + { url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" }, + { url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" }, + { url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" }, + { url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" }, + { url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/eb4e394e587341fdad09a09101fa76478ead3a78b0ad63e55c22f0d75c02/cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a", size = 3951747, upload-time = "2026-06-09T22:31:23.871Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" }, + { url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8c/ce3823c06c2804f194f9e64f0d67fa3f4094a39f2bb1a990cd03603af8fc/cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a", size = 3742204, upload-time = "2026-06-09T22:31:34.773Z" }, ] [[package]] @@ -1465,16 +1481,16 @@ wheels = [ [[package]] name = "msal" -version = "1.35.1" +version = "1.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/aa/5a646093ac218e4a329391d5a31e5092a89db7d2ef1637a90b82cd0b6f94/msal-1.35.1.tar.gz", hash = "sha256:70cac18ab80a053bff86219ba64cfe3da1f307c74b009e2da57ef040eb1b5656", size = 165658, upload-time = "2026-03-04T23:38:51.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/99/d840198ecf6e8057bbc937f129ae940404485d736cda73253bbff9537f01/msal-1.37.0.tar.gz", hash = "sha256:1b1672a33ee467c1d70b341bb16cafd51bb3c817147a95b93263794b03971bec", size = 182444, upload-time = "2026-05-29T19:49:05.561Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/86/16815fddf056ca998853c6dc525397edf0b43559bb4073a80d2bc7fe8009/msal-1.35.1-py3-none-any.whl", hash = "sha256:8f4e82f34b10c19e326ec69f44dc6b30171f2f7098f3720ea8a9f0c11832caa3", size = 119909, upload-time = "2026-03-04T23:38:50.452Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/d807279f4b55d16d1f120d5ac4344c6e39b56732e2a224d40bded7fd67ad/msal-1.37.0-py3-none-any.whl", hash = "sha256:dd17e95a7c71bce75e8108113438ba7c4a086b3bcad4f57a8c09b7af3d753c2d", size = 123725, upload-time = "2026-05-29T19:49:04.335Z" }, ] [[package]] @@ -2413,16 +2429,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] @@ -2436,11 +2452,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.12.1" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] [package.optional-dependencies] @@ -2527,11 +2543,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.29" +version = "0.0.31" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/7e/9b35ad8f3d9ca680f7c87a88f19612fdd8da9796c4d3b46e560ac79dcc4a/python_multipart-0.0.31.tar.gz", hash = "sha256:fc631183bb13e56db3158a4909908dfb2e23565286744e798241e63750e5d680", size = 46689, upload-time = "2026-06-04T08:27:49.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/7f7f299527a5a8ad90acd5f2f78dfa6c8495c6301a3205106ea68a84de96/python_multipart-0.0.31-py3-none-any.whl", hash = "sha256:8408153d68a9773291fc1da39a8b85a50044bddbabd2dd72e9229776b7b15e28", size = 29996, upload-time = "2026-06-04T08:27:47.804Z" }, ] [[package]] @@ -2989,15 +3005,15 @@ wheels = [ [[package]] name = "starlette" -version = "1.2.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/bf/616a066c2760f6c2b1ae3437cc28149734d069fbb46511712beae118a68c/starlette-1.2.0.tar.gz", hash = "sha256:3c5a6b23fff42492914e93890bb80cbfea72dbf37de268eec06185d62a4ca553", size = 2668923, upload-time = "2026-05-28T11:42:50.568Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/85/492183764d5d01d4514be3730fdb8e228a80605783099551c51627578b5d/starlette-1.2.0-py3-none-any.whl", hash = "sha256:36e0c76ac59157e75dc4b3bdeafba97fb04eaf1878045f15dbef666a6f092ed7", size = 73213, upload-time = "2026-05-28T11:42:48.801Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] [[package]] From 41dce8203067bb45060a68b43da5896077c05e68 Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Fri, 26 Jun 2026 13:51:38 -0300 Subject: [PATCH 6/9] Add retrieval telemetry imports for evaluations (#341) * Add retrieval telemetry imports for evaluations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix telemetry import column typing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 9 + README.md | 6 +- docs/concepts.md | 14 +- docs/evaluation.md | 257 ++++++++ ...ndry-evaluation-sdk-built-in-evaluators.md | 351 ++++++----- src/agentops/cli/app.py | 107 ++++ src/agentops/core/agentops_config.py | 131 +++++ src/agentops/pipeline/invocations.py | 10 +- src/agentops/pipeline/orchestrator.py | 78 ++- src/agentops/pipeline/runtime.py | 28 +- src/agentops/services/telemetry_import.py | 550 ++++++++++++++++++ tests/unit/test_agentops_config.py | 109 +++- tests/unit/test_cli_commands.py | 100 ++++ tests/unit/test_http_response_fields.py | 106 ++++ tests/unit/test_runtime_conversation.py | 25 + .../test_runtime_dataset_response_source.py | 74 +++ tests/unit/test_telemetry_import.py | 153 +++++ 17 files changed, 1899 insertions(+), 209 deletions(-) create mode 100644 docs/evaluation.md create mode 100644 src/agentops/services/telemetry_import.py create mode 100644 tests/unit/test_http_response_fields.py create mode 100644 tests/unit/test_runtime_dataset_response_source.py create mode 100644 tests/unit/test_telemetry_import.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d1de82bd..aeb264ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ This format follows [Keep a Changelog](https://keepachangelog.com/) and adheres ## [Unreleased] +### Added +- **Retrieval telemetry can now be imported as evaluation datasets.** The new + `telemetry_imports` config contract and `agentops telemetry validate`, + `agentops telemetry preview`, and `agentops telemetry import` commands let + teams turn reviewed retrieval telemetry into dataset-backed eval rows with + `response_source: dataset`. Grey-box HTTP agents can map `response_fields` from + `$response.context`, and the evaluation docs now cover the import workflow and + contract. + ### Changed - **PR-stage Foundry prompt-agent versions are now tagged at the source.** When `agentops.pipeline.prompt_deploy stage` runs in a PR context (GitHub Actions diff --git a/README.md b/README.md index df83e83f..e7428c8a 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,10 @@ Doctor/Cockpit, and `[mcp]` for MCP. - `agentops eval analyze` - check eval readiness. - `agentops eval init` - bootstrap an azd `eval.yaml` recipe and wire `execution: azd`. - `agentops eval run [--baseline PATH]` - run an evaluation. -- `agentops eval promote-traces --source FILE [--apply]` - promote traces. +- `agentops eval promote-traces --source FILE [--apply]` - promote local trace export files. +- `agentops telemetry validate NAME` - validate an Azure Monitor or Application Insights import. +- `agentops telemetry preview NAME --rows N` - preview telemetry import rows. +- `agentops telemetry import NAME --apply` - write the imported telemetry dataset. - `agentops report generate` - regenerate `report.md`. - `agentops workflow analyze` - recommend CI/CD shape. - `agentops workflow generate` - generate CI/CD workflows. @@ -217,6 +220,7 @@ Cockpit sections, in display order: - [Foundry Prompt Agent tutorial](docs/tutorial-prompt-agent-quickstart.md) - use this when the Foundry target is `agent: name:version`. Walks the sandbox → dev journey with a PR gate. - [Hosted or HTTP Agent tutorial](docs/tutorial-hosted-agent-quickstart.md) - use this when the target is a Foundry hosted or HTTP endpoint URL. Same sandbox → dev journey for endpoint-based agents. - [End-to-end tutorial](docs/tutorial-end-to-end.md) - extends either of the above with the full sandbox → dev → qa → prod promotion, Foundry red-team scans, and trace-to-regression promotion. +- [Evaluation paths](docs/evaluation.md) - choose static dataset, grey-box HTTP, or telemetry/trace import. - [Core concepts](docs/concepts.md) - [How it works](docs/how-it-works.md) - [Doctor explained](docs/doctor-explained.md) diff --git a/docs/concepts.md b/docs/concepts.md index 93be6d3d..39ce4efa 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -96,8 +96,10 @@ Common `agent:` values: | `"model:gpt-4o-mini"` | Direct model deployment | HTTP targets can add top-level mapping fields such as `request_field`, -`response_field`, `tool_calls_field`, `auth_header_env`, and -`extra_fields`. +`response_fields`, `tool_calls_field`, `auth_header_env`, and `extra_fields`. +Use `response_fields.response` for the final answer and +`response_fields.context` for retrieved context. Use `response_source: dataset` +when each dataset row already contains the response to evaluate. ### Dataset @@ -198,6 +200,8 @@ AgentOps auto-selects common evaluation patterns from the dataset: Use one of the three hands-on tutorials for scenario coverage: +- [Evaluation paths](evaluation.md) explains when to use a static dataset, + grey-box HTTP response mapping, or telemetry/trace import. - [Foundry Prompt Agent tutorial](tutorial-prompt-agent-quickstart.md) for Foundry prompt agents referenced as `name:version`. - [Hosted or HTTP Agent tutorial](tutorial-hosted-agent-quickstart.md) for Foundry @@ -215,9 +219,13 @@ the fields your target needs: version: 1 agent: "https://api.example.com/chat" dataset: .agentops/data/support.jsonl +response_source: agent +protocol: http-json request_field: message -response_field: text +response_fields: + response: text + context: retrieved_context thresholds: coherence: ">=3" diff --git a/docs/evaluation.md b/docs/evaluation.md new file mode 100644 index 00000000..aedaee64 --- /dev/null +++ b/docs/evaluation.md @@ -0,0 +1,257 @@ +# Evaluation + +Use this page when you need to choose how AgentOps should evaluate a RAG or +agent workflow. The goal is simple: pick the path that matches where your +evidence comes from, run the evaluation, and keep the result in a format that +reviewers can trust. + +AgentOps supports three evaluation paths: + +1. **Static dataset**: use a JSONL file that already contains the prompt, + expected answer, and optional retrieval context. +2. **Grey-box HTTP**: call an HTTP endpoint and extract both the answer and + retrieval context from the live response. +3. **Telemetry/trace import**: import production traces into a reviewable + dataset so real traffic can become future regression coverage. + +## Choose a path + +| Path | Use it when | Best first step | +|---|---|---| +| Static dataset | You already know the test cases, expected answers, and optionally the target responses. | Create or edit `.agentops/data/*.jsonl`. | +| Grey-box HTTP | Your endpoint can return the answer plus retrieval details for the same request. | Configure `request_field` and `response_fields`. | +| Telemetry/trace import | You want to learn from production traffic before adding new regression rows. | Configure `telemetry_imports`, then run `agentops telemetry preview`. | + +The paths build on each other. Most teams start with a static dataset, add +grey-box HTTP when they need retrieval telemetry, then use telemetry import after +the agent is running in production. + +```mermaid +flowchart LR + Static[Static dataset] --> HTTP[Grey-box HTTP] + HTTP --> Traces[Telemetry import] + Traces --> Static +``` + +## Static dataset + +Choose this path when the data you need is already in the dataset file. Each row +is a test case. AgentOps sends `input` to the target, compares the target +response with `expected`, and uses `context` when present to select RAG +evaluators. + +By default, `response_source: agent` means AgentOps calls the configured target. +Use `response_source: dataset` only when the dataset already includes the answer +you want to evaluate in a `response`, `prediction`, `output`, or `answer` field. +That is useful for offline review or imported trace rows that should not call a +live endpoint again. + +Minimal RAG row: + +```json +{"id":"refund-001","input":"What is the refund window?","expected":"Customers can request a refund within 30 days.","context":"Refunds are available for 30 days after purchase."} +``` + +Minimal config: + +```yaml +version: 1 +agent: "support-agent:3" +dataset: .agentops/data/rag-smoke.jsonl +response_source: agent + +thresholds: + groundedness: ">=3" + retrieval: ">=3" + response_completeness: ">=3" +``` + +Run it: + +```powershell +agentops eval analyze +agentops eval run +``` + +Use this path for: + +- Fast local checks before opening a PR. +- CI gates with stable examples. +- Baseline comparison with `agentops eval run --baseline`. +- Manual review of newly written or newly labeled examples. + +## Grey-box HTTP + +Choose this path when the endpoint can return more than final text. This is the +best path for RAG services because the evaluator can see what the agent actually +retrieved for the request. + +The endpoint response should include: + +- the final answer; +- retrieval context, citations, or document chunks; +- optional tool calls or workflow metadata. + +Example endpoint response: + +```json +{ + "answer": "Customers can request a refund within 30 days.", + "context": [ + "Refunds are available for 30 days after purchase.", + "Refunds require the original order number." + ], + "citations": ["refund-policy.md"] +} +``` + +Example config: + +```yaml +version: 1 +agent: "https://support-dev.example.com/chat" +dataset: .agentops/data/rag-smoke.jsonl + +protocol: http-json +request_field: message +response_fields: + response: answer + context: context + citations: citations + +thresholds: + groundedness: ">=3" + retrieval: ">=3" + relevance: ">=3" +``` + +What happens: + +1. AgentOps reads each row from the dataset. +2. It sends `row.input` as the HTTP request field named by `request_field`. +3. It extracts the final answer from `response_fields.response`. +4. It extracts retrieval context from `response_fields.context`. +5. RAG evaluators can use the extracted context through `$response.context`, + `$retrieved_context`, or `$retrieved_context_items`. + +Use dot paths when fields are nested: + +```yaml +response_fields: + response: output.text + context: output.retrieval.chunks +``` + +Use this path for: + +- RAG services where the retrieved chunks matter. +- Debugging why a groundedness or retrieval score changed. +- Endpoint-based agents hosted in Azure Container Apps, AKS, Foundry Hosted + Agents, or another HTTP host. + +## Telemetry import + +Choose this path when production traffic has useful examples that are not yet in +your test set. Telemetry import does not make production responses automatically +correct. It creates reviewable dataset candidates. + +Configure a named telemetry import in `agentops.yaml`: + +```yaml +telemetry_imports: + - name: prod-rag + target: application-insights + resource_id: $APPINSIGHTS_RESOURCE_ID + time_range: + lookback_days: 7 + filters: + customDimensions.agent: support-agent + fields: + input: customDimensions.question + response: customDimensions.answer + context: customDimensions.retrieved_context + trace_id: operation_Id + output: + path: .agentops/data/prod-rag-candidates.jsonl + label_mode: pending +``` + +Validate the import without querying Azure: + +```powershell +agentops telemetry validate prod-rag +``` + +Preview rows from Azure Monitor: + +```powershell +agentops telemetry preview prod-rag --rows 10 +``` + +Write the candidate dataset and manifest: + +```powershell +agentops telemetry import prod-rag --apply +``` + +Label modes: + +| Mode | What it writes | Use it when | +|---|---|---| +| `pending` | Empty `expected` values with review metadata. | A human must write the correct answer before the row can gate a release. | +| `self-similarity` | The production response becomes `expected`. | You want drift detection against known production behavior. | + +Telemetry import keeps lineage metadata such as trace ID, timestamp, replay URL, +and source system when those values exist in the export. If the trace includes +retrieval context, AgentOps writes it as `context` so RAG evaluators can use it +later. Evaluator mappings can also use `$telemetry.trace_id` when a trace ID is +needed for reporting or troubleshooting. + +If you already have a local trace export file, `agentops eval promote-traces` +still works. Use `agentops telemetry` when the source is Azure Monitor or +Application Insights. + +Use this path for: + +- Turning incidents or surprising production answers into regression tests. +- Sampling real traffic for future review. +- Building a trace-to-dataset flywheel without skipping human judgment. + +## Input mapping + +Evaluator inputs come from three places: + +| Source | Placeholder | Example | +|---|---|---| +| Dataset prompt | `$row.input` or `$prompt` | User question sent to the agent. | +| Dataset expected answer | `$row.expected` or `$expected` | Ground truth or acceptance criteria. | +| Agent response | `$response.response` or `$prediction` | Final answer returned by the target. | +| Any response field | `$response.` | Any field extracted through `response_fields`. | +| Extracted retrieval context | `$response.context`, `$retrieved_context`, or `$retrieved_context_items` | Chunks, citations, or grounding text from the live response. | +| Dataset retrieval context | `$row.context` | Static context stored in JSONL. | +| Trace ID | `$telemetry.trace_id` | Azure Monitor or Application Insights operation ID. | + +For beginners, the easiest rule is: + +- Put known test data in the dataset. +- Put live endpoint outputs under `response_fields`. +- Let AgentOps map the common fields to evaluators. + +Only customize evaluator selection when the automatic choice is not enough: + +```yaml +evaluators: + - GroundednessEvaluator + - RetrievalEvaluator + - RelevanceEvaluator +``` + +## Safety notes + +- Do not treat production responses as ground truth without review. +- Do not import sensitive trace payloads into a repository dataset. +- Keep secrets in environment variables or `.agentops/.env`, not in JSONL files. +- Prefer `--label-mode pending` when correctness matters. +- Use `self-similarity` only for drift detection. +- Keep trace replay links in metadata so reviewers can investigate the original + runtime behavior. diff --git a/docs/foundry-evaluation-sdk-built-in-evaluators.md b/docs/foundry-evaluation-sdk-built-in-evaluators.md index 87cfe859..e6cba1ad 100644 --- a/docs/foundry-evaluation-sdk-built-in-evaluators.md +++ b/docs/foundry-evaluation-sdk-built-in-evaluators.md @@ -1,218 +1,199 @@ -# Foundry Evaluation SDK Built-in Evaluators (AgentOps) +# Foundry Evaluators -This guide maps Microsoft Foundry built-in evaluators to the configuration model used by AgentOps Toolkit. +This page explains how AgentOps maps Microsoft Foundry Evaluation SDK +evaluators to the data in `agentops.yaml`, dataset rows, HTTP responses, and +trace imports. -## 1) AgentOps config shape (quick reference) +Most users do not need to configure evaluator internals. AgentOps selects common +evaluators from the target type and dataset shape. Use this page when you need +to understand what each evaluator receives. -In AgentOps, each evaluator is configured under `bundle.evaluators[]`: +## Config shape + +The normal config stays small: ```yaml -evaluators: - - name: SimilarityEvaluator - source: foundry - enabled: true - config: - kind: builtin # builtin | custom - class_name: SimilarityEvaluator - init: # constructor kwargs - model_config: - azure_endpoint: ${env:AZURE_OPENAI_ENDPOINT} - azure_deployment: ${env:AZURE_OPENAI_DEPLOYMENT} - input_mapping: # evaluator call kwargs - query: $prompt - response: $prediction - ground_truth: $expected - score_keys: # ordered keys to read numeric score - - similarity - - score +version: 1 +agent: "https://support-dev.example.com/chat" +dataset: .agentops/data/rag-smoke.jsonl +response_source: agent + +protocol: http-json +request_field: message +response_fields: + response: answer + context: context + +thresholds: + groundedness: ">=3" + retrieval: ">=3" + coherence: ">=3" ``` -## 2) Global requirements by evaluator family - -- AI-assisted quality evaluators use a judge model (`model_config`) in Azure OpenAI/OpenAI schema. -- Risk/safety evaluators and `GroundednessProEvaluator` use `azure_ai_project` instead of GPT deployment in `model_config`. -- Agent evaluators require agent-style payloads (`query/response` as messages, and often tool metadata). -- NLP evaluators (`F1`, `BLEU`, `GLEU`, `ROUGE`, `METEOR`) are non-LLM evaluators and usually need `response` + `ground_truth`. - -## 3) Built-in evaluators and required AgentOps parameters - -| Evaluator | Category | Typical required inputs | Backend init requirements | AgentOps `config` minimum | -|---|---|---|---|---| -| `CoherenceEvaluator` | General purpose | `query`, `response` | `model_config` (AI-assisted) | `kind: builtin`, `class_name`, `input_mapping(query,response)`, `score_keys` | -| `FluencyEvaluator` | General purpose | `query`, `response` | `model_config` (AI-assisted) | same as above | -| `SimilarityEvaluator` | Textual similarity | `query`, `response`, `ground_truth` | `model_config` (AI-assisted) | `input_mapping(query,response,ground_truth)` | -| `F1ScoreEvaluator` | Textual similarity (NLP) | `response`, `ground_truth` | none beyond class init defaults | `input_mapping(response,ground_truth)` | -| `BleuScoreEvaluator` | Textual similarity (NLP) | `response`, `ground_truth` | none beyond class init defaults | `input_mapping(response,ground_truth)` | -| `GleuScoreEvaluator` | Textual similarity (NLP) | `response`, `ground_truth` | none beyond class init defaults | `input_mapping(response,ground_truth)` | -| `RougeScoreEvaluator` | Textual similarity (NLP) | `response`, `ground_truth` | optional `rouge_type` in `init` | `input_mapping(response,ground_truth)` | -| `MeteorScoreEvaluator` | Textual similarity (NLP) | `response`, `ground_truth` | optional `alpha/beta/gamma/threshold` in `init` | `input_mapping(response,ground_truth)` | -| `RetrievalEvaluator` | RAG | usually `query`, `response`, `context` | `model_config` (AI-assisted) | `input_mapping(query,response,context)` | -| `DocumentRetrievalEvaluator` | RAG | retrieval outputs + `ground_truth` | check SDK class contract | explicit `input_mapping` recommended | -| `GroundednessEvaluator` | RAG | `response`, `context` (optional `query`) | `model_config` (AI-assisted) | `input_mapping(response,context[,query])` | -| `GroundednessProEvaluator` *(preview)* | RAG | `query/response/context` or conversation | `azure_ai_project` (not GPT model_config) | `init.azure_ai_project`, explicit `input_mapping` | -| `RelevanceEvaluator` | RAG | `query`, `response` (optionally `context`) | `model_config` (AI-assisted) | `input_mapping(query,response[,context])` | -| `ResponseCompletenessEvaluator` | RAG | `response`, `ground_truth` (often query optional) | AI-assisted quality path | explicit `input_mapping`, `score_keys` | -| `HateUnfairnessEvaluator` | Safety | `query`, `response`/conversation | `azure_ai_project` | `init.azure_ai_project`, `input_mapping` | -| `SexualEvaluator` | Safety | `query`, `response`/conversation | `azure_ai_project` | same | -| `ViolenceEvaluator` | Safety | `query`, `response`/conversation | `azure_ai_project` | same | -| `SelfHarmEvaluator` | Safety | `query`, `response`/conversation | `azure_ai_project` | same | -| `ContentSafetyEvaluator` | Safety composite | query/response or conversation | `azure_ai_project` | `init.azure_ai_project`, `input_mapping` | -| `ProtectedMaterialEvaluator` | Safety | query/response or multimodal | `azure_ai_project` | `init.azure_ai_project`, `input_mapping` | -| `CodeVulnerabilityEvaluator` | Safety/risk | text/code response | `azure_ai_project` | `init.azure_ai_project`, `input_mapping(response[,query])` | -| `UngroundedAttributesEvaluator` | Safety/risk | text response | `azure_ai_project` | `init.azure_ai_project`, `input_mapping(response[,query])` | -| `IndirectAttackEvaluator` | Safety/risk | conversation-oriented input | `azure_ai_project` | `init.azure_ai_project`, `input_mapping(conversation/query,response)` | -| `IntentResolutionEvaluator` *(preview)* | Agent | `query`, `response` (string or message list) | agent evaluator path | `input_mapping(query,response[,tool_definitions])` | -| `TaskAdherenceEvaluator` *(preview)* | Agent | `query`, `response` (string or message list) | agent evaluator path | `input_mapping(query,response[,tool_calls])` | -| `ToolCallAccuracyEvaluator` *(preview)* | Agent | `query`; plus `response` or `tool_calls`; `tool_definitions` required | agent evaluator path | `input_mapping(query,response,tool_calls,tool_definitions)` | -| `TaskCompletionEvaluator` *(preview)* | Agent | agent run/conversation payload | preview; use latest SDK docs | explicit `input_mapping`, explicit `score_keys` | -| `TaskNavigationEfficiencyEvaluator` *(preview)* | Agent | tool/call sequence + expected path context | preview; evolving | explicit `input_mapping`, explicit `score_keys` | -| `ToolSelectionEvaluator` *(preview)* | Agent | query/response + selected tools + tool defs | preview; evolving | explicit `input_mapping`, explicit `score_keys` | -| `ToolInputAccuracyEvaluator` *(preview)* | Agent | tool args + tool defs + context | preview; evolving | explicit `input_mapping`, explicit `score_keys` | -| `ToolOutputUtilizationEvaluator` *(preview)* | Agent | tool outputs + final response | preview; evolving | explicit `input_mapping`, explicit `score_keys` | -| `ToolCallSuccessEvaluator` *(preview)* | Agent | tool execution results/status | preview; evolving | explicit `input_mapping`, explicit `score_keys` | -| `QAEvaluator` | Composite quality | `query`, `response`, `ground_truth`, `context` | `model_config` (AI-assisted composite) | `input_mapping(query,response,ground_truth,context)` | -| `AzureOpenAILabelGrader` | Azure OpenAI grader | template-driven (often conversation/query/response) | grader init requires template/model config | explicit `init` + explicit `input_mapping` | -| `AzureOpenAIStringCheckGrader` | Azure OpenAI grader | template-driven text fields | grader init requires template | explicit `init` + explicit `input_mapping` | -| `AzureOpenAITextSimilarityGrader` | Azure OpenAI grader | text + `ground_truth` equivalent | grader init requires template/model config | explicit `init` + explicit `input_mapping` | -| `AzureOpenAIGrader` | Azure OpenAI grader | template-defined | grader init requires rubric/template | explicit `init` + explicit `input_mapping` | - -## 4) Practical rules for AgentOps bundles - -- Always set `source: foundry` for Foundry SDK evaluators. -- For preview evaluators, always provide explicit: - - `config.class_name` - - `config.input_mapping` - - `config.score_keys` -- Prefer explicit `input_mapping` even when defaults might work. -- Keep `thresholds[].evaluator` exactly equal to `evaluators[].name`. -- For agent evaluators, use structured fields in dataset rows (messages, tool calls, tool definitions) and map with `$row.`. - -## 5) Examples by evaluator type - -The following examples show one practical bundle snippet for each evaluator family used in AgentOps: - -- `5.1` AI-assisted quality evaluators (`model_config`) -- `5.2` Risk/safety evaluators (`azure_ai_project`) -- `5.3` Agent evaluators (message/tool payloads) -- `5.4` NLP evaluators (non-LLM) - -## 5.1) Example for AI-assisted quality evaluator (`model_config`) +Use `evaluators:` only when you want to override the automatic choice: ```yaml evaluators: - - name: RelevanceEvaluator - source: foundry - enabled: true - config: - kind: builtin - class_name: RelevanceEvaluator - init: - model_config: - azure_endpoint: ${env:AZURE_OPENAI_ENDPOINT} - azure_deployment: ${env:AZURE_OPENAI_DEPLOYMENT} - input_mapping: - query: $prompt - response: $prediction - score_keys: - - relevance - - score + - GroundednessEvaluator + - RetrievalEvaluator + - RelevanceEvaluator +``` -thresholds: - - evaluator: RelevanceEvaluator - criteria: ">=" - value: 3 +## Evaluator families + +| Family | What it checks | Common inputs | +|---|---|---| +| Quality judges | The answer is coherent, fluent, similar, complete, or relevant. | prompt, response, expected answer | +| RAG judges | The answer uses retrieved context and the retrieval is useful. | prompt, response, context | +| Safety judges | The answer avoids harmful or protected content. | prompt, response | +| Agent judges | Tool use and agent workflow behavior are correct. | prompt, response, tool calls, tool definitions | +| Local metrics | Scores that do not need a judge model. | response, expected answer, latency | + +## Evaluator inputs + +AgentOps uses a small set of logical inputs. The same logical input can come from +a static dataset, a live HTTP response, or imported telemetry. + +| Logical input | Meaning | Common source | +|---|---|---| +| `query` | The user prompt. | `row.input` | +| `response` | The target's final answer. | extracted response text | +| `ground_truth` | The expected answer or acceptance criteria. | `row.expected` | +| `response field` | Any value extracted through `response_fields`. | `$response.` | +| `context` | Retrieved chunks, citations, or grounding text. | `row.context`, `$response.context`, `$retrieved_context`, or `$retrieved_context_items` | +| `tool_calls` | Tools called by the agent. | endpoint response or dataset row | +| `tool_definitions` | Tool schemas available to the agent. | dataset row | +| `trace_id` | Trace lineage for review and troubleshooting. | `$telemetry.trace_id` | + +## Mapping rules + +The mapping rules are intentionally boring: + +1. `input` in the dataset becomes the evaluator `query`. +2. The extracted target answer becomes `response`. +3. `expected` in the dataset becomes `ground_truth`. +4. `context` in the dataset becomes evaluator `context`. +5. For grey-box HTTP, `response_fields.response` supplies the final answer. +6. For grey-box HTTP, `response_fields.context` can supply `$response.context`. +7. `$retrieved_context` and `$retrieved_context_items` expose retrieval context + in the evaluator placeholder format. +8. `$telemetry.trace_id` exposes imported telemetry lineage when it exists. +9. Tool fields are used only when the dataset or response includes tool data. + +For RAG, prefer a live context from the response when the endpoint can return it. +That gives the judge the same evidence the agent used for the answer. Use static +`row.context` when you want a fixed, hand-authored reference context. + +## Examples + +Static dataset row: + +```json +{"input":"What is the refund window?","expected":"Customers can request a refund within 30 days.","context":"Refunds are available for 30 days after purchase."} ``` -## 5.2) Example for risk/safety evaluator (`azure_ai_project`) +Static dataset config: ```yaml -evaluators: - - name: ViolenceEvaluator - source: foundry - enabled: true - config: - kind: builtin - class_name: ViolenceEvaluator - init: - azure_ai_project: ${env:AZURE_AI_FOUNDRY_PROJECT_ENDPOINT} - input_mapping: - query: $prompt - response: $prediction - score_keys: - - violence - - score - -thresholds: - - evaluator: ViolenceEvaluator - criteria: "<=" - value: 2 +response_source: dataset ``` -`GroundednessProEvaluator` follows the same pattern (`azure_ai_project` in `init`). +Use `response_source: dataset` when each row already has a `response`, +`prediction`, `output`, or `answer` value and AgentOps should evaluate that value +instead of calling the target. -## 5.3) Example for agent evaluator (agent payload + tools) +Grey-box HTTP config: ```yaml -evaluators: - - name: ToolCallAccuracyEvaluator - source: foundry - enabled: true - config: - kind: builtin - class_name: ToolCallAccuracyEvaluator - input_mapping: - query: $row.query_messages - response: $row.response_messages - tool_calls: $row.tool_calls - tool_definitions: $row.tool_definitions - score_keys: - - tool_call_accuracy - - score +protocol: http-json +request_field: message +response_fields: + response: output.answer + context: output.retrieval.chunks +``` -thresholds: - - evaluator: ToolCallAccuracyEvaluator - criteria: ">=" - value: 3 +Telemetry import: + +```powershell +agentops telemetry validate prod-rag +agentops telemetry preview prod-rag --rows 10 +agentops telemetry import prod-rag --apply ``` -## 5.4) Example for NLP evaluator (non-LLM) +## Quality judges -```yaml -evaluators: - - name: F1ScoreEvaluator - source: foundry - enabled: true - config: - kind: builtin - class_name: F1ScoreEvaluator - input_mapping: - response: $prediction - ground_truth: $expected - score_keys: - - f1_score - - score +| Evaluator | Typical inputs | Notes | +|---|---|---| +| `CoherenceEvaluator` | `query`, `response` | Checks whether the answer is logically consistent. | +| `FluencyEvaluator` | `response` | Checks language quality. | +| `SimilarityEvaluator` | `query`, `response`, `ground_truth` | Compares the answer with the expected answer. | +| `ResponseCompletenessEvaluator` | `query`, `response`, `ground_truth` | Checks whether the answer covers what was expected. | +| `RelevanceEvaluator` | `query`, `response`, optional `context` | Useful for both chat and RAG quality. | -thresholds: - - evaluator: F1ScoreEvaluator - criteria: ">=" - value: 0.7 -``` +Quality judges need a judge model deployment. Set +`AZURE_OPENAI_DEPLOYMENT` or `AZURE_AI_MODEL_DEPLOYMENT_NAME` when local or +cloud evaluation needs one. -## 6) Cloud Evaluation defaults +## Safety judges -AgentOps provides sensible defaults so you don't need to configure extra environment variables: +| Evaluator | Typical inputs | Notes | +|---|---|---| +| `ViolenceEvaluator` | `query`, `response` | Scores violent content risk. | +| `SexualEvaluator` | `query`, `response` | Scores sexual content risk. | +| `SelfHarmEvaluator` | `query`, `response` | Scores self-harm content risk. | +| `HateUnfairnessEvaluator` | `query`, `response` | Scores hate and unfairness risk. | +| `ProtectedMaterialEvaluator` | `query`, `response` | Checks protected material risk when supported by the SDK. | +| `ContentSafetyEvaluator` | `query`, `response` | Composite safety path when supported by the SDK. | -| Setting | Default | Override | +Safety judges require a Foundry project connection. Use +`AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` or `project_endpoint:` in `agentops.yaml`. + +## Agent judges + +| Evaluator | Typical inputs | Notes | |---|---|---| -| Judge model (AI-assisted evaluators) | A deployment you configure in your project | `AZURE_OPENAI_DEPLOYMENT` or `AZURE_AI_MODEL_DEPLOYMENT_NAME` env var | -| Authentication | `DefaultAzureCredential` (passwordless) | `az login` locally, Managed Identity in Azure | +| `ToolCallAccuracyEvaluator` | `query`, `tool_calls`, `tool_definitions` | Checks whether the expected tools were called. | +| `IntentResolutionEvaluator` | `query`, `response`, `tool_definitions` | Checks whether the agent resolved the user's intent. | +| `TaskAdherenceEvaluator` | `query`, `response`, `tool_definitions` | Checks whether the agent stayed on task. | +| `TaskCompletionEvaluator` | conversation payload | Preview in some SDK versions. | +| `ToolSelectionEvaluator` | tool selection plus tool definitions | Preview in some SDK versions. | +| `ToolInputAccuracyEvaluator` | tool arguments plus tool definitions | Preview in some SDK versions. | -## 7) Known caveats +Agent judges work best when the target returns tool telemetry or the dataset row +contains expected tool calls. If the endpoint cannot expose tool calls, start +with answer quality and RAG judges instead. + +## Local metrics + +| Evaluator | Typical inputs | Notes | +|---|---|---| +| `F1ScoreEvaluator` | `response`, `ground_truth` | Good for exact reference checks. | +| `BleuScoreEvaluator` | `response`, `ground_truth` | Optional text similarity metric. | +| `GleuScoreEvaluator` | `response`, `ground_truth` | Optional text similarity metric. | +| `RougeScoreEvaluator` | `response`, `ground_truth` | Optional summary similarity metric. | +| `MeteorScoreEvaluator` | `response`, `ground_truth` | Optional text similarity metric. | +| `avg_latency_seconds` | elapsed time | AgentOps computes this locally. | + +Local metrics are useful when you want a cheap deterministic signal. They are not +a replacement for human review or RAG-specific judges. + +## Cloud defaults + +AgentOps keeps cloud evaluation setup minimal: + +| Setting | Default | Override | +|---|---|---| +| Authentication | `DefaultAzureCredential` | `az login` locally, managed identity in Azure, or federated identity in CI. | +| Foundry project | `project_endpoint` or `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` | Set either value before running. | +| Judge model | Project deployment selected by environment | `AZURE_OPENAI_DEPLOYMENT` or `AZURE_AI_MODEL_DEPLOYMENT_NAME`. | +| Publishing | Implicit for `execution: cloud` | `publish: true` for local runs that should upload metrics. | -- Some agent evaluators listed in the latest Foundry docs are preview and can change name/signature. -- Not all preview evaluators have stable Python API docs with full constructor/call signatures at any given time. -- When a signature changes, update the evaluator override list in `agentops.yaml` (no code change is needed in AgentOps core; the runtime is generic). +## Caveats -**Last updated:** 2026-03-02 (UTC) +- Foundry Evaluation SDK preview evaluators can change names or call signatures. +- If the SDK changes an evaluator, keep the docs, catalog, and tests in sync. +- `response_fields.response` is the final answer path for HTTP JSON responses. +- `response_fields.context` is the retrieved context path for RAG evaluation. +- Production trace imports need review before they become blocking release gates. -Because Foundry Evaluation SDK and evaluator signatures evolve (especially preview features), review official docs before production rollout. +**Last updated:** 2026-06-26 (UTC) diff --git a/src/agentops/cli/app.py b/src/agentops/cli/app.py index b40f3869..e2a473ed 100644 --- a/src/agentops/cli/app.py +++ b/src/agentops/cli/app.py @@ -75,6 +75,9 @@ "for the manual." ) ) +telemetry_app = typer.Typer( + help="Import Azure Monitor telemetry into AgentOps datasets." +) app.add_typer(eval_app, name="eval") app.add_typer(report_app, name="report") app.add_typer(workflow_app, name="workflow") @@ -85,6 +88,7 @@ app.add_typer(init_app, name="init") app.add_typer(assert_app, name="assert") app.add_typer(redteam_app, name="redteam") +app.add_typer(telemetry_app, name="telemetry") log = get_logger(__name__) DEFAULT_REPORT_INPUT = Path(".agentops/results/latest/results.json") @@ -2237,6 +2241,109 @@ def cmd_eval_promote_traces( ) +@telemetry_app.command("validate") +def cmd_telemetry_validate( + name: Annotated[str, typer.Argument(help="Name under telemetry_imports.")], + config: Annotated[ + Optional[Path], + typer.Option("--config", "-c", help="Path to agentops.yaml."), + ] = None, +) -> None: + """Validate a named telemetry import without querying Azure.""" + + from agentops.core.config_loader import load_agentops_config + from agentops.services.telemetry_import import ( + TelemetryImportError, + find_telemetry_import, + validate_telemetry_import, + ) + + try: + cfg = load_agentops_config(_resolve_eval_config_path(config)) + item = find_telemetry_import(cfg, name) + warnings = validate_telemetry_import(item) + except (TelemetryImportError, ValueError) as exc: + typer.echo(_cli_error(str(exc)), err=True) + raise typer.Exit(1) from exc + typer.echo(_cli_ok(f"telemetry import {name!r} is valid")) + for warning in warnings: + typer.echo(_cli_warn(f"warning: {warning}")) + + +@telemetry_app.command("preview") +def cmd_telemetry_preview( + name: Annotated[str, typer.Argument(help="Name under telemetry_imports.")], + rows: Annotated[int, typer.Option("--rows", min=1, help="Maximum rows to preview.")] = 10, + config: Annotated[ + Optional[Path], + typer.Option("--config", "-c", help="Path to agentops.yaml."), + ] = None, +) -> None: + """Query Azure Monitor and print a small dataset preview.""" + + from agentops.core.config_loader import load_agentops_config + from agentops.services.telemetry_import import ( + TelemetryImportError, + find_telemetry_import, + preview_telemetry_import, + render_telemetry_import_preview, + ) + + try: + cfg = load_agentops_config(_resolve_eval_config_path(config)) + item = find_telemetry_import(cfg, name) + preview = preview_telemetry_import(item, rows=rows, apply=False) + except (TelemetryImportError, ValueError) as exc: + typer.echo(_cli_error(str(exc)), err=True) + raise typer.Exit(1) from exc + typer.echo(render_telemetry_import_preview(preview)) + + +@telemetry_app.command("import") +def cmd_telemetry_import( + name: Annotated[str, typer.Argument(help="Name under telemetry_imports.")], + apply: Annotated[ + bool, + typer.Option("--apply", help="Write JSONL rows and manifest."), + ] = False, + rows: Annotated[ + Optional[int], + typer.Option("--rows", min=1, help="Optional maximum rows to import."), + ] = None, + config: Annotated[ + Optional[Path], + typer.Option("--config", "-c", help="Path to agentops.yaml."), + ] = None, +) -> None: + """Import telemetry into the configured JSONL output path.""" + + from agentops.core.config_loader import load_agentops_config + from agentops.services.telemetry_import import ( + TelemetryImportError, + find_telemetry_import, + preview_telemetry_import, + render_telemetry_import_preview, + ) + + if not apply: + typer.echo( + _cli_warn( + "Dry run only. Re-run with --apply to write the JSONL dataset and manifest." + ) + ) + try: + cfg = load_agentops_config(_resolve_eval_config_path(config)) + item = find_telemetry_import(cfg, name) + preview = preview_telemetry_import(item, rows=rows, apply=apply) + except (TelemetryImportError, ValueError) as exc: + typer.echo(_cli_error(str(exc)), err=True) + raise typer.Exit(1) from exc + typer.echo(render_telemetry_import_preview(preview)) + if apply: + typer.echo(_cli_updated(preview.output_path)) + typer.echo(_cli_updated(preview.manifest_path)) + + def _resolve_eval_config_path(config: Path | None) -> Path: if config is not None: return config diff --git a/src/agentops/core/agentops_config.py b/src/agentops/core/agentops_config.py index 57670e8d..f7268ecb 100644 --- a/src/agentops/core/agentops_config.py +++ b/src/agentops/core/agentops_config.py @@ -70,6 +70,14 @@ #: Dataset shape used by the evaluator runtime or Foundry / azd recipes. DatasetKind = Literal["auto", "single-turn", "multi-turn"] +#: Where the local evaluator runtime gets the response text for each row. +ResponseSource = Literal["agent", "dataset"] + +#: Production telemetry import providers and destinations. +TelemetrySourceProvider = Literal["azure-monitor"] +TelemetryTarget = Literal["application-insights", "log-analytics"] +TelemetryLabelMode = Literal["self-similarity", "pending"] + #: Internal-only literal kept for the publisher dispatch table. Derived from #: ``execution`` + ``publish`` via :meth:`AgentOpsConfig.publish_target`. PublishTarget = Literal["foundry", "foundry_cloud"] @@ -316,6 +324,116 @@ def _url_non_empty(cls, value: Optional[str]) -> Optional[str]: return value +# --------------------------------------------------------------------------- +# Telemetry import configuration +# --------------------------------------------------------------------------- + + +class TelemetryTimeRangeConfig(BaseModel): + """Time window for a telemetry import query. + + Users can either provide explicit ISO-ish ``from``/``to`` timestamps or a + relative ``lookback_days`` window. The service owns final KQL rendering so + users never pass arbitrary query text. + """ + + from_: Optional[str] = Field(None, alias="from") + to: Optional[str] = None + lookback_days: Optional[int] = Field(None, ge=1, le=90) + + model_config = ConfigDict(extra="forbid", populate_by_name=True) + + @model_validator(mode="after") + def _validate_window(self) -> "TelemetryTimeRangeConfig": + explicit = self.from_ is not None or self.to is not None + if explicit and not (self.from_ and self.to): + raise ValueError("telemetry_imports.time_range requires both from and to") + if explicit and self.lookback_days is not None: + raise ValueError("telemetry_imports.time_range cannot mix from/to with lookback_days") + if not explicit and self.lookback_days is None: + self.lookback_days = 7 + return self + + +class TelemetryPrivacyConfig(BaseModel): + """Privacy controls applied before JSONL rows are written.""" + + redact_fields: List[str] = Field( + default_factory=lambda: ["authorization", "api_key", "token", "password", "secret"], + description="Case-insensitive field-name fragments to redact.", + ) + max_field_length: int = Field(4000, ge=100, le=20000) + include_raw: bool = False + + model_config = ConfigDict(extra="forbid") + + +class TelemetryOutputConfig(BaseModel): + """Output paths and labeling mode for generated dataset rows.""" + + path: Path = Field(Path(".agentops") / "data" / "telemetry-import.jsonl") + manifest_path: Optional[Path] = None + label_mode: TelemetryLabelMode = "self-similarity" + + model_config = ConfigDict(extra="forbid") + + +class TelemetryImportConfig(BaseModel): + """Named telemetry import declaration. + + The MVP intentionally keeps this declarative: users choose a supported + source/destination pair, field mappings, filters, privacy settings, and an + output file. The service generates the KQL. + """ + + name: str + source: TelemetrySourceProvider = "azure-monitor" + target: TelemetryTarget + resource_id: Optional[str] = None + workspace_id: Optional[str] = None + application_id: Optional[str] = None + connection_string: Optional[str] = None + time_range: TelemetryTimeRangeConfig = Field(default_factory=TelemetryTimeRangeConfig) + filters: Dict[str, str | List[str]] = Field(default_factory=dict) + fields: Dict[str, str] = Field(default_factory=dict) + privacy: TelemetryPrivacyConfig = Field(default_factory=TelemetryPrivacyConfig) + output: TelemetryOutputConfig = Field(default_factory=TelemetryOutputConfig) + max_rows: int = Field(100, ge=1, le=5000) + + model_config = ConfigDict(extra="forbid") + + @field_validator("name") + @classmethod + def _name_non_empty(cls, value: str) -> str: + value = value.strip() + if not value: + raise ValueError("telemetry_imports.name must be non-empty") + return value + + @field_validator("resource_id", "workspace_id", "application_id", "connection_string") + @classmethod + def _optional_text_non_empty(cls, value: Optional[str]) -> Optional[str]: + if value is None: + return value + value = value.strip() + if not value: + raise ValueError("telemetry_imports resource identifiers must be non-empty") + return value + + @model_validator(mode="after") + def _validate_target_ids(self) -> "TelemetryImportConfig": + if self.target == "log-analytics" and not self.workspace_id: + raise ValueError("telemetry_imports targeting log-analytics require workspace_id") + if self.target == "application-insights" and not ( + self.resource_id or self.application_id or self.connection_string + ): + raise ValueError( + "telemetry_imports targeting application-insights require resource_id, " + "application_id, or connection_string" + ) + return self + + class PromptAgentBootstrap(BaseModel): """Bootstrap defaults for prompt-agent CI/CD when the target Foundry project does not yet contain the seed agent referenced by ``agent``. @@ -631,6 +749,13 @@ class AgentOpsConfig(BaseModel): version: int = Field(..., description="Schema version. Must be 1.") agent: str = Field(..., description="Target identifier (name:version, URL, or model:deployment)") dataset: Path = Field(..., description="Path to a JSONL dataset file") + response_source: ResponseSource = Field( + "agent", + description=( + "Where local eval gets each response. 'agent' invokes the configured " + "target. 'dataset' uses each row's response or prediction value." + ), + ) dataset_kind: DatasetKind = Field( "auto", description=( @@ -693,11 +818,16 @@ class AgentOpsConfig(BaseModel): protocol: Optional[Protocol] = None request_field: Optional[str] = None response_field: Optional[str] = None + response_fields: Dict[str, str] = Field(default_factory=dict) tool_calls_field: Optional[str] = None headers: Dict[str, str] = Field(default_factory=dict) auth_header_env: Optional[str] = None evaluators: Optional[List[EvaluatorOverride]] = None + telemetry_imports: List[TelemetryImportConfig] = Field( + default_factory=list, + description="Named Azure Monitor imports that generate AgentOps JSONL datasets.", + ) rubrics: List[RubricConfig] = Field( default_factory=list, description="Optional context-specific rubric evaluator definitions.", @@ -847,6 +977,7 @@ def _validate_protocol_compat(self) -> "AgentOpsConfig": if kind != "http_json" and ( self.request_field or self.response_field + or self.response_fields or self.tool_calls_field or self.headers or self.auth_header_env diff --git a/src/agentops/pipeline/invocations.py b/src/agentops/pipeline/invocations.py index 0eb590e8..23d131e2 100644 --- a/src/agentops/pipeline/invocations.py +++ b/src/agentops/pipeline/invocations.py @@ -483,7 +483,7 @@ def _invoke_http_json( ) elapsed = time.perf_counter() - started - response_path = config.response_field or "text" + response_path = config.response_field or config.response_fields.get("response") or "text" response_text = _dot_path(payload, response_path) if response_text is None: for fallback in ("response", "output", "content", "message", "text"): @@ -498,6 +498,13 @@ def _invoke_http_json( if not isinstance(response_text, str): response_text = json.dumps(response_text, ensure_ascii=False) + response_fields: Dict[str, Any] = {} + for name, path in config.response_fields.items(): + value = _dot_path(payload, path) + if value is not None: + response_fields[name] = value + response_fields.setdefault("response", response_text) + tool_calls: Optional[List[Any]] = None if config.tool_calls_field: extracted = _dot_path(payload, config.tool_calls_field) @@ -508,6 +515,7 @@ def _invoke_http_json( response=response_text.strip(), latency_seconds=elapsed, tool_calls=tool_calls, + metadata={"response_fields": response_fields} if response_fields else {}, ) diff --git a/src/agentops/pipeline/orchestrator.py b/src/agentops/pipeline/orchestrator.py index 81dc32e8..806c5173 100644 --- a/src/agentops/pipeline/orchestrator.py +++ b/src/agentops/pipeline/orchestrator.py @@ -718,7 +718,8 @@ def _evaluate_row( preview = str(row.get("input", "")).strip().replace("\n", " ") if len(preview) > 80: preview = preview[:77] + "..." - progress(f"{label} invoking target: {preview!r}") + action = "using dataset response" if config.response_source == "dataset" else "invoking target" + progress(f"{label} {action}: {preview!r}") expected = row.get("expected") expected_text = str(expected) if expected is not None else None @@ -728,18 +729,21 @@ def _evaluate_row( expected_text=expected_text, ) as item_span: try: - with telemetry.agent_invoke_span( - target="agent" if target.kind.startswith("foundry") else "model", - model=target.deployment, - agent_id=target.raw if target.kind.startswith("foundry") else None, - agent_name=target.name, - agent_version=target.version, - ) as invoke_span: - invocation = invocations.invoke(target, config, row, timeout=timeout) - telemetry.set_agent_invoke_result( - invoke_span, - response_model=target.deployment, - ) + if config.response_source == "dataset": + invocation = _dataset_invocation(row) + else: + with telemetry.agent_invoke_span( + target="agent" if target.kind.startswith("foundry") else "model", + model=target.deployment, + agent_id=target.raw if target.kind.startswith("foundry") else None, + agent_name=target.name, + agent_version=target.version, + ) as invoke_span: + invocation = invocations.invoke(target, config, row, timeout=timeout) + telemetry.set_agent_invoke_result( + invoke_span, + response_model=target.deployment, + ) except Exception as exc: # noqa: BLE001 telemetry.set_eval_item_result(item_span, passed=False) logger.debug("row %d invocation failed: %s", index, exc) @@ -759,11 +763,16 @@ def _evaluate_row( f"({tool_count} tool call(s)); scoring..." ) + response_fields = invocation.metadata.get("response_fields") + evaluator_row = row + if isinstance(response_fields, dict) and response_fields: + evaluator_row = {**row, "response": response_fields} + metrics: List[RowMetric] = [] for evaluator in evaluators: metric = runtime.run_evaluator( evaluator, - row=row, + row=evaluator_row, response=invocation.response, latency_seconds=invocation.latency_seconds, actual_tool_calls=invocation.tool_calls, @@ -814,18 +823,57 @@ def _format_metric(m: RowMetric) -> str: scored = ", ".join(_format_metric(m) for m in metrics) progress(f"{label} scored: {scored}") + result_context = ( + response_fields.get("context") + if isinstance(response_fields, dict) and response_fields.get("context") is not None + else row.get("context") + ) + return RowResult( row_index=index, input=str(row.get("input", "")), expected=row.get("expected"), response=invocation.response, - context=row.get("context"), + context=_context_as_text(result_context), latency_seconds=invocation.latency_seconds, tool_calls=invocation.tool_calls, metrics=metrics, ) +def _dataset_invocation(row: Dict[str, Any]) -> invocations.InvocationResult: + """Build an invocation result from dataset columns without calling a target.""" + + response = row.get("response") + if response is None: + response = row.get("prediction") + if response is None: + raise ValueError( + "response_source: dataset requires each dataset row to contain " + "a response or prediction field" + ) + + tool_calls = row.get("actual_tool_calls") + if tool_calls is None: + tool_calls = row.get("tool_calls") + if tool_calls is not None and not isinstance(tool_calls, list): + tool_calls = None + return invocations.InvocationResult( + response=str(response), + latency_seconds=0.0, + tool_calls=tool_calls, + metadata={"response_source": "dataset"}, + ) + + +def _context_as_text(value: Any) -> Optional[str]: + if value is None: + return None + if isinstance(value, str): + return value + return json.dumps(value, ensure_ascii=False) + + # --------------------------------------------------------------------------- # Aggregation # --------------------------------------------------------------------------- diff --git a/src/agentops/pipeline/runtime.py b/src/agentops/pipeline/runtime.py index b22e4dbe..ba986b36 100644 --- a/src/agentops/pipeline/runtime.py +++ b/src/agentops/pipeline/runtime.py @@ -195,8 +195,11 @@ def load_evaluators(presets: List[EvaluatorPreset]) -> List[EvaluatorRuntime]: "$prediction": "response", "$expected": "expected", "$context": "context", + "$retrieved_context": "retrieved_context", + "$retrieved_context_items": "retrieved_context_items", "$tool_calls": "tool_calls", "$tool_definitions": "tool_definitions", + "$telemetry.trace_id": "telemetry.trace_id", } @@ -294,21 +297,40 @@ def _resolve_kwargs( response: str, ) -> Dict[str, Any]: resolved: Dict[str, Any] = {} + row_response = row.get("response") merged = {**row, "response": response, "input": row.get("input")} for kwarg, placeholder in mapping.items(): if not isinstance(placeholder, str) or not placeholder.startswith("$"): resolved[kwarg] = placeholder continue - source_key = _PLACEHOLDERS.get(placeholder) - if source_key is None: + source_path = _PLACEHOLDERS.get(placeholder) + if source_path is None and placeholder.startswith("$response."): + if isinstance(row_response, dict): + value = _lookup_placeholder(row_response, placeholder[len("$response."):]) + if value is not None: + resolved[kwarg] = value + continue + source_path = placeholder[1:] + if source_path is None and placeholder.startswith("$telemetry."): + source_path = placeholder[1:] + if source_path is None: raise ValueError(f"unknown evaluator placeholder {placeholder!r}") - value = merged.get(source_key) + value = _lookup_placeholder(merged, source_path) if value is None: continue resolved[kwarg] = value return resolved +def _lookup_placeholder(data: Dict[str, Any], path: str) -> Any: + current: Any = data + for part in path.split("."): + if not isinstance(current, dict): + return None + current = current.get(part) + return current + + def _extract_score(payload: Any, score_key: str) -> Optional[float]: if payload is None: return None diff --git a/src/agentops/services/telemetry_import.py b/src/agentops/services/telemetry_import.py new file mode 100644 index 00000000..286778c5 --- /dev/null +++ b/src/agentops/services/telemetry_import.py @@ -0,0 +1,550 @@ +"""Import Azure Monitor telemetry into AgentOps JSONL datasets. + +The module has two halves: + +* a pure transformer that maps telemetry rows into AgentOps dataset rows +* a thin Azure Monitor query wrapper with lazy SDK imports + +Users never provide raw KQL. The query builder only accepts structured time +ranges, field mappings, filters, and row limits from ``agentops.yaml``. +""" + +from __future__ import annotations + +import json +import os +import re +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Iterable, Optional + +from agentops.core.agentops_config import AgentOpsConfig, TelemetryImportConfig + +DEFAULT_MAX_ROWS = 100 +MAX_ROWS_CAP = 5000 + +_DEFAULT_FIELD_CANDIDATES: dict[str, tuple[str, ...]] = { + "input": ( + "input", + "query", + "prompt", + "message", + "user_message", + "customDimensions.input", + "customDimensions.query", + "customDimensions.prompt", + "customDimensions.gen_ai.prompt", + ), + "response": ( + "response", + "prediction", + "output", + "answer", + "completion", + "assistant_message", + "customDimensions.response", + "customDimensions.prediction", + "customDimensions.output", + "customDimensions.gen_ai.completion", + ), + "context": ( + "context", + "retrieved_context", + "grounding", + "customDimensions.context", + "customDimensions.retrieved_context", + "customDimensions.grounding", + ), + "retrieved_context_items": ( + "retrieved_context_items", + "context_items", + "customDimensions.retrieved_context_items", + "customDimensions.context_items", + ), + "tool_calls": ("tool_calls", "customDimensions.tool_calls"), + "trace_id": ("trace_id", "operation_Id", "operationId"), + "turn_id": ("turn_id", "span_id", "id", "customDimensions.turn_id"), + "timestamp": ("timestamp", "TimeGenerated", "time"), +} + +_QUERY_COLUMNS = ( + "timestamp", + "operation_Id = column_ifexists('operation_Id', '')", + "operationId = column_ifexists('operationId', '')", + "id = column_ifexists('id', '')", + "name = column_ifexists('name', '')", + "message = column_ifexists('message', '')", + "duration = column_ifexists('duration', '')", + "success = column_ifexists('success', '')", + "customDimensions = column_ifexists('customDimensions', dynamic({}))", +) + + +class TelemetryImportError(RuntimeError): + """Raised when a telemetry import cannot be validated, queried, or written.""" + + +@dataclass(frozen=True) +class TelemetryImportPreview: + """Result of validating/querying/transforming one telemetry import.""" + + config: TelemetryImportConfig + output_path: Path + manifest_path: Path + rows: list[dict[str, Any]] + skipped: int = 0 + deduped: int = 0 + truncated: bool = False + warnings: list[str] = field(default_factory=list) + + +def find_telemetry_import( + config: AgentOpsConfig, + name: str, +) -> TelemetryImportConfig: + """Return a named telemetry import or raise a friendly error.""" + + for item in config.telemetry_imports: + if item.name == name: + return item + available = ", ".join(item.name for item in config.telemetry_imports) or "none" + raise TelemetryImportError( + f"telemetry import {name!r} was not found in agentops.yaml. " + f"Available imports: {available}." + ) + + +def validate_telemetry_import(config: TelemetryImportConfig) -> list[str]: + """Validate service-level constraints and return non-fatal warnings.""" + + warnings: list[str] = [] + if config.output.label_mode == "self-similarity": + warnings.append( + "Generated rows use production responses as expected values for drift " + "detection, not human-verified ground truth." + ) + return warnings + + +def preview_telemetry_import( + config: TelemetryImportConfig, + *, + rows: Optional[int] = None, + apply: bool = False, +) -> TelemetryImportPreview: + """Query Azure Monitor, transform rows, and optionally write JSONL output.""" + + validate_telemetry_import(config) + raw_rows = query_azure_monitor(config, rows=rows) + preview = transform_telemetry_rows(config, raw_rows, rows=rows) + if apply: + write_telemetry_import(preview) + return preview + + +def transform_telemetry_rows( + config: TelemetryImportConfig, + telemetry_rows: Iterable[dict[str, Any]], + *, + rows: Optional[int] = None, +) -> TelemetryImportPreview: + """Pure transformation from telemetry records to AgentOps dataset rows.""" + + limit = _bounded_rows(rows if rows is not None else config.max_rows) + output_path = config.output.path + manifest_path = config.output.manifest_path or output_path.with_name( + f"{output_path.stem}-manifest.json" + ) + warnings = validate_telemetry_import(config) + converted: list[dict[str, Any]] = [] + skipped = 0 + deduped = 0 + seen: set[tuple[str, str]] = set() + + for raw in telemetry_rows: + if len(converted) >= limit: + break + row = _telemetry_row_to_agentops_row(config, raw) + if row is None: + skipped += 1 + continue + telemetry = row.get("telemetry") + trace_id = "" + turn_id = "" + if isinstance(telemetry, dict): + trace_id = str(telemetry.get("trace_id") or "") + turn_id = str(telemetry.get("turn_id") or "") + key = (trace_id or row["input"], turn_id or row.get("response", "")) + if key in seen: + deduped += 1 + continue + seen.add(key) + converted.append(row) + + truncated = len(converted) >= limit + if not converted: + warnings.append("No telemetry rows contained both input and response text.") + return TelemetryImportPreview( + config=config, + output_path=output_path, + manifest_path=manifest_path, + rows=converted, + skipped=skipped, + deduped=deduped, + truncated=truncated, + warnings=warnings, + ) + + +def write_telemetry_import(preview: TelemetryImportPreview) -> None: + """Write JSONL rows and a small manifest next to the output.""" + + preview.output_path.parent.mkdir(parents=True, exist_ok=True) + with preview.output_path.open("w", encoding="utf-8") as handle: + for row in preview.rows: + handle.write(json.dumps(row, ensure_ascii=False) + "\n") + + trace_ids = [ + str(row.get("telemetry", {}).get("trace_id")) + for row in preview.rows + if isinstance(row.get("telemetry"), dict) and row["telemetry"].get("trace_id") + ] + manifest = { + "version": 1, + "generated_at": datetime.now(timezone.utc).isoformat(), + "import": preview.config.name, + "source": preview.config.source, + "target": preview.config.target, + "output_path": str(preview.output_path), + "rows": len(preview.rows), + "skipped": preview.skipped, + "deduped": preview.deduped, + "truncated": preview.truncated, + "trace_ids": trace_ids, + "warnings": preview.warnings, + } + preview.manifest_path.write_text( + json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + + +def render_telemetry_import_preview(preview: TelemetryImportPreview) -> str: + """Render concise CLI output.""" + + lines = [ + "AgentOps telemetry import", + f"Import: {preview.config.name}", + f"Target: {preview.config.target}", + f"Output: {preview.output_path}", + "", + "Summary", + f" rows {len(preview.rows)}", + f" skipped {preview.skipped}", + f" deduped {preview.deduped}", + f" truncated {str(preview.truncated).lower()}", + ] + if preview.warnings: + lines.append("") + lines.append("Warnings") + lines.extend(f" - {warning}" for warning in preview.warnings) + if preview.rows: + lines.append("") + lines.append("Sample rows") + for index, row in enumerate(preview.rows[:3], start=1): + lines.append(f" {index}. {str(row.get('input', ''))[:120]}") + return "\n".join(lines) + "\n" + + +def query_azure_monitor( + config: TelemetryImportConfig, + *, + rows: Optional[int] = None, +) -> list[dict[str, Any]]: + """Run the generated KQL against Azure Monitor with lazy SDK imports.""" + + try: + from azure.identity import DefaultAzureCredential # noqa: WPS433 + except ImportError as exc: + raise TelemetryImportError( + "Telemetry import requires Azure authentication packages. Install " + "them with: python -m pip install azure-identity azure-monitor-query" + ) from exc + + kql = build_telemetry_kql(config, rows=rows) + credential = DefaultAzureCredential( + exclude_developer_cli_credential=True, + process_timeout=30, + ) + try: + if config.target == "log-analytics": + from azure.monitor.query import LogsQueryClient # noqa: WPS433 + + client = LogsQueryClient(credential) + workspace_id = _resolve_value(config.workspace_id, "workspace_id") + response = client.query_workspace(workspace_id, kql, timespan=None) + return _flatten_logs_response(response) + if config.resource_id: + from azure.monitor.query import LogsQueryClient # noqa: WPS433 + + client = LogsQueryClient(credential) + resource_id = _resolve_value(config.resource_id, "resource_id") + response = client.query_resource(resource_id, kql, timespan=None) + return _flatten_logs_response(response) + app_id = _application_id(config) + token = credential.get_token("https://api.applicationinsights.io/.default").token + return _query_application_insights(app_id, token, kql) + except ImportError as exc: + raise TelemetryImportError( + "Telemetry import with resource_id/workspace_id requires the Azure " + "Monitor Query SDK. Install it with: python -m pip install " + "azure-monitor-query" + ) from exc + except TelemetryImportError: + raise + except Exception as exc: # noqa: BLE001 + raise TelemetryImportError(f"Azure Monitor query failed: {exc}") from exc + + +def build_telemetry_kql( + config: TelemetryImportConfig, + *, + rows: Optional[int] = None, +) -> str: + """Build safe KQL from structured config only.""" + + limit = _bounded_rows(rows if rows is not None else config.max_rows) + clauses = ["union isfuzzy=true requests, dependencies, traces"] + clauses.append(f"| extend timestamp = {_timestamp_expr()}") + clauses.append(_time_clause(config)) + for key, value in sorted(config.filters.items()): + clauses.append(_filter_clause(key, value)) + columns = ", ".join(_QUERY_COLUMNS) + clauses.append(f"| project {columns}") + clauses.append("| order by timestamp desc") + clauses.append(f"| take {limit}") + return "\n".join(clauses) + + +def _telemetry_row_to_agentops_row( + config: TelemetryImportConfig, + raw: dict[str, Any], +) -> Optional[dict[str, Any]]: + input_text = _mapped_text(config, raw, "input") + response_text = _mapped_text(config, raw, "response") + if not input_text or not response_text: + return None + + label_mode = config.output.label_mode + telemetry = { + "trace_id": _mapped_text(config, raw, "trace_id"), + "turn_id": _mapped_text(config, raw, "turn_id"), + "timestamp": _mapped_text(config, raw, "timestamp"), + "source": config.source, + "target": config.target, + "import": config.name, + } + row: dict[str, Any] = { + "input": _clean_value(input_text, config), + "response": _clean_value(response_text, config), + "prediction": _clean_value(response_text, config), + "expected": _clean_value(response_text, config) if label_mode == "self-similarity" else "", + "telemetry": {k: v for k, v in telemetry.items() if v not in (None, "")}, + "metadata": { + "source": "azure_monitor_telemetry", + "label_mode": label_mode, + "needs_review": True, + }, + } + context = _mapped_value(config, raw, "context") + if context not in (None, "", [], {}): + row["context"] = _clean_value(context, config) + row["retrieved_context"] = row["context"] + context_items = _mapped_value(config, raw, "retrieved_context_items") + if context_items not in (None, "", [], {}): + row["retrieved_context_items"] = _clean_value(context_items, config) + tool_calls = _mapped_value(config, raw, "tool_calls") + if tool_calls not in (None, "", [], {}): + row["tool_calls"] = _clean_value(tool_calls, config) + if config.privacy.include_raw: + row["raw"] = _clean_value(raw, config) + return row + + +def _mapped_text(config: TelemetryImportConfig, raw: dict[str, Any], name: str) -> Optional[str]: + value = _mapped_value(config, raw, name) + if value is None: + return None + if isinstance(value, str): + value = value.strip() + return value or None + if isinstance(value, (dict, list)): + text = json.dumps(value, ensure_ascii=False) + return text if text not in ("{}", "[]") else None + text = str(value).strip() + return text or None + + +def _mapped_value(config: TelemetryImportConfig, raw: dict[str, Any], name: str) -> Any: + mapping = config.fields.get(name) + if mapping: + return _lookup(raw, mapping) + for candidate in _DEFAULT_FIELD_CANDIDATES.get(name, ()): + value = _lookup(raw, candidate) + if value not in (None, "", [], {}): + return value + return None + + +def _lookup(data: dict[str, Any], path: str) -> Any: + current: Any = data + for part in path.split("."): + if not isinstance(current, dict): + return None + current = current.get(part) + return current + + +def _clean_value(value: Any, config: TelemetryImportConfig, key: str = "") -> Any: + lowered = key.lower() + if any(fragment.lower() in lowered for fragment in config.privacy.redact_fields): + return "[redacted]" + if isinstance(value, dict): + return {k: _clean_value(v, config, str(k)) for k, v in value.items()} + if isinstance(value, list): + return [_clean_value(item, config, key) for item in value] + if isinstance(value, str) and len(value) > config.privacy.max_field_length: + return value[: config.privacy.max_field_length] + "...[truncated]" + return value + + +def _flatten_logs_response(response: Any) -> list[dict[str, Any]]: + tables = getattr(response, "tables", None) or [] + if not tables: + return [] + table = tables[0] + columns: list[str] = [] + for column in getattr(table, "columns", None) or []: + name = getattr(column, "name", None) if not isinstance(column, dict) else column.get("name") + if isinstance(name, str): + columns.append(name) + rows: list[dict[str, Any]] = [] + for raw in getattr(table, "rows", None) or []: + rows.append(dict(zip(columns, raw))) + return rows + + +def _application_id(config: TelemetryImportConfig) -> str: + if config.application_id: + return _resolve_value(config.application_id, "application_id") + if config.connection_string: + connection_string = _resolve_value(config.connection_string, "connection_string") + match = re.search(r"ApplicationId=([0-9a-fA-F-]+)", connection_string) + if match: + return match.group(1) + raise TelemetryImportError( + "application-insights imports require resource_id, application_id, or " + "a connection_string containing ApplicationId" + ) + + +def _query_application_insights(app_id: str, bearer: str, kql: str) -> list[dict[str, Any]]: + import json as _json + from urllib import request + + body = _json.dumps({"query": kql}).encode("utf-8") + req = request.Request( + url=f"https://api.applicationinsights.io/v1/apps/{app_id}/query", + data=body, + headers={ + "Authorization": f"Bearer {bearer}", + "Content-Type": "application/json", + }, + method="POST", + ) + with request.urlopen(req, timeout=30) as response: # noqa: S310 + parsed = _json.loads(response.read()) + if isinstance(parsed, dict) and parsed.get("error"): + err = parsed["error"] + message = err.get("message") if isinstance(err, dict) else str(err) + raise TelemetryImportError(f"Application Insights query failed: {message}") + tables = parsed.get("tables") if isinstance(parsed, dict) else None + if not tables: + return [] + table = tables[0] + columns = [column.get("name") for column in table.get("columns", [])] + return [dict(zip(columns, row)) for row in table.get("rows", [])] + + +def _time_clause(config: TelemetryImportConfig) -> str: + tr = config.time_range + if tr.from_ and tr.to: + return ( + f"| where timestamp between (datetime({_kql_string(tr.from_)}) .. " + f"datetime({_kql_string(tr.to)}))" + ) + days = tr.lookback_days or 7 + return f"| where timestamp >= ago({days}d)" + + +def _filter_clause(key: str, value: str | list[str]) -> str: + expr = _safe_column_expr(key) + values = value if isinstance(value, list) else [value] + escaped = ", ".join(_kql_string(str(item)) for item in values) + if len(values) == 1: + return f"| where {expr} == {escaped}" + return f"| where {expr} in ({escaped})" + + +def _safe_column_expr(key: str) -> str: + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?", key): + raise TelemetryImportError( + f"unsafe telemetry filter field {key!r}; use a column name or customDimensions.name" + ) + if key.startswith("customDimensions."): + subkey = key.split(".", 1)[1] + return ( + "tostring(column_ifexists('customDimensions', dynamic({}))" + f"[{_kql_string(subkey)}])" + ) + return f"tostring(column_ifexists({_kql_string(key)}, ''))" + + +def _timestamp_expr() -> str: + return ( + "coalesce(" + "column_ifexists('timestamp', datetime(null)), " + "column_ifexists('TimeGenerated', datetime(null)), " + "column_ifexists('time', datetime(null))" + ")" + ) + + +def _kql_string(value: str) -> str: + return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'" + + +def _resolve_value(value: Optional[str], label: str) -> str: + if not value: + raise TelemetryImportError(f"telemetry import is missing {label}") + value = value.strip() + env_name: Optional[str] = None + if value.startswith("env:"): + env_name = value[4:] + elif value.startswith("$") and len(value) > 1: + env_name = value[1:].strip("{}") + if env_name: + resolved = os.getenv(env_name) + if not resolved: + raise TelemetryImportError( + f"environment variable {env_name} referenced by {label} is not set" + ) + return resolved + return value + + +def _bounded_rows(rows: int) -> int: + if rows <= 0: + raise TelemetryImportError("rows must be greater than zero") + return min(rows, MAX_ROWS_CAP) diff --git a/tests/unit/test_agentops_config.py b/tests/unit/test_agentops_config.py index 288f56a2..5fbc5458 100644 --- a/tests/unit/test_agentops_config.py +++ b/tests/unit/test_agentops_config.py @@ -141,6 +141,111 @@ def test_minimal_config(self, tmp_path) -> None: assert cfg.version == 1 assert cfg.agent == "my-rag:3" assert cfg.thresholds == {} + assert cfg.response_source == "agent" + assert cfg.telemetry_imports == [] + + def test_accepts_telemetry_import_config(self) -> None: + cfg = AgentOpsConfig.model_validate( + { + "version": 1, + "agent": "my-rag:3", + "dataset": "./qa.jsonl", + "response_source": "dataset", + "telemetry_imports": [ + { + "name": "prod", + "source": "azure-monitor", + "target": "application-insights", + "resource_id": "$APPINSIGHTS_RESOURCE_ID", + "time_range": {"lookback_days": 14}, + "filters": {"customDimensions.agent": "support"}, + "fields": { + "input": "customDimensions.question", + "response": "customDimensions.answer", + }, + "privacy": {"redact_fields": ["token"], "max_field_length": 500}, + "output": { + "path": ".agentops/data/prod.jsonl", + "label_mode": "pending", + }, + } + ], + } + ) + + item = cfg.telemetry_imports[0] + assert cfg.response_source == "dataset" + assert item.name == "prod" + assert item.source == "azure-monitor" + assert item.target == "application-insights" + assert item.resource_id == "$APPINSIGHTS_RESOURCE_ID" + assert item.time_range.lookback_days == 14 + assert item.output.label_mode == "pending" + + def test_telemetry_import_rejects_unknown_fields(self) -> None: + with pytest.raises(ValidationError): + AgentOpsConfig.model_validate( + { + "version": 1, + "agent": "my-rag:3", + "dataset": "./qa.jsonl", + "telemetry_imports": [ + { + "name": "prod", + "target": "log-analytics", + "workspace_id": "workspace", + "surprise": True, + } + ], + } + ) + + def test_telemetry_import_time_range_requires_one_mode(self) -> None: + with pytest.raises(ValidationError, match="cannot mix"): + AgentOpsConfig.model_validate( + { + "version": 1, + "agent": "my-rag:3", + "dataset": "./qa.jsonl", + "telemetry_imports": [ + { + "name": "prod", + "target": "log-analytics", + "workspace_id": "workspace", + "time_range": { + "from": "2026-06-01T00:00:00Z", + "to": "2026-06-02T00:00:00Z", + "lookback_days": 7, + }, + } + ], + } + ) + + def test_telemetry_import_accepts_explicit_time_range(self) -> None: + cfg = AgentOpsConfig.model_validate( + { + "version": 1, + "agent": "my-rag:3", + "dataset": "./qa.jsonl", + "telemetry_imports": [ + { + "name": "prod", + "target": "log-analytics", + "workspace_id": "workspace", + "time_range": { + "from": "2026-06-01T00:00:00Z", + "to": "2026-06-02T00:00:00Z", + }, + } + ], + } + ) + + time_range = cfg.telemetry_imports[0].time_range + assert time_range.from_ == "2026-06-01T00:00:00Z" + assert time_range.to == "2026-06-02T00:00:00Z" + assert time_range.lookback_days is None def test_resolved_target(self) -> None: cfg = AgentOpsConfig(version=1, agent="my-rag:3", dataset="./qa.jsonl") @@ -472,8 +577,10 @@ def test_http_fields_allowed_for_http_target(self) -> None: dataset="./qa.jsonl", request_field="message", response_field="text", + response_fields={"context": "retrieval.context"}, ) assert cfg.request_field == "message" + assert cfg.response_fields == {"context": "retrieval.context"} def test_http_fields_rejected_for_prompt_agent(self) -> None: with pytest.raises(ValidationError, match="HTTP/JSON"): @@ -481,7 +588,7 @@ def test_http_fields_rejected_for_prompt_agent(self) -> None: version=1, agent="my-rag:3", dataset="./qa.jsonl", - request_field="message", + response_fields={"context": "context"}, ) def test_evaluators_override(self) -> None: diff --git a/tests/unit/test_cli_commands.py b/tests/unit/test_cli_commands.py index 0c6b2a68..3bb6feb7 100644 --- a/tests/unit/test_cli_commands.py +++ b/tests/unit/test_cli_commands.py @@ -1,6 +1,7 @@ from typer.testing import CliRunner from agentops.cli.app import app +from agentops.services.telemetry_import import TelemetryImportPreview runner = CliRunner() @@ -83,3 +84,102 @@ def test_agent_command_group_wired() -> None: stripped = _strip_ansi(result.stdout) assert "analyze" in stripped assert "serve" in stripped + + +def test_telemetry_validate_uses_named_import(tmp_path, monkeypatch) -> None: + config = tmp_path / "agentops.yaml" + config.write_text( + "\n".join( + [ + "version: 1", + "agent: support-agent:1", + "dataset: .agentops/data/smoke.jsonl", + "telemetry_imports:", + " - name: prod", + " target: log-analytics", + " workspace_id: workspace", + ] + ), + encoding="utf-8", + ) + monkeypatch.setattr( + "agentops.services.telemetry_import.validate_telemetry_import", + lambda _item: [], + ) + + result = runner.invoke(app, ["telemetry", "validate", "prod", "--config", str(config)]) + + assert result.exit_code == 0, result.output + assert "prod" in result.output + assert "valid" in result.output + + +def test_telemetry_preview_prints_service_preview(tmp_path, monkeypatch) -> None: + config = tmp_path / "agentops.yaml" + config.write_text( + "version: 1\n" + "agent: support-agent:1\n" + "dataset: .agentops/data/smoke.jsonl\n" + "telemetry_imports:\n" + " - name: prod\n" + " target: log-analytics\n" + " workspace_id: workspace\n", + encoding="utf-8", + ) + + def fake_preview(item, *, rows=None, apply=False): + return TelemetryImportPreview( + config=item, + output_path=tmp_path / "prod.jsonl", + manifest_path=tmp_path / "prod-manifest.json", + rows=[{"input": "hello", "response": "world"}], + ) + + monkeypatch.setattr( + "agentops.services.telemetry_import.preview_telemetry_import", + fake_preview, + ) + + result = runner.invoke( + app, + ["telemetry", "preview", "prod", "--rows", "1", "--config", str(config)], + ) + + assert result.exit_code == 0, result.output + assert "AgentOps telemetry import" in result.output + assert "hello" in result.output + + +def test_telemetry_import_requires_apply_to_write(tmp_path, monkeypatch) -> None: + config = tmp_path / "agentops.yaml" + config.write_text( + "version: 1\n" + "agent: support-agent:1\n" + "dataset: .agentops/data/smoke.jsonl\n" + "telemetry_imports:\n" + " - name: prod\n" + " target: log-analytics\n" + " workspace_id: workspace\n", + encoding="utf-8", + ) + calls = [] + + def fake_preview(item, *, rows=None, apply=False): + calls.append(apply) + return TelemetryImportPreview( + config=item, + output_path=tmp_path / "prod.jsonl", + manifest_path=tmp_path / "prod-manifest.json", + rows=[], + ) + + monkeypatch.setattr( + "agentops.services.telemetry_import.preview_telemetry_import", + fake_preview, + ) + + result = runner.invoke(app, ["telemetry", "import", "prod", "--config", str(config)]) + + assert result.exit_code == 0, result.output + assert calls == [False] + assert "Dry run only" in result.output diff --git a/tests/unit/test_http_response_fields.py b/tests/unit/test_http_response_fields.py new file mode 100644 index 00000000..abbdb324 --- /dev/null +++ b/tests/unit/test_http_response_fields.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +from agentops.core.agentops_config import AgentOpsConfig, classify_agent +from agentops.core.evaluators import EvaluatorPreset +from agentops.pipeline import invocations, orchestrator, runtime + + +def test_http_json_captures_named_response_fields(monkeypatch) -> None: + cfg = AgentOpsConfig( + version=1, + agent="https://example.test/chat", + dataset="./qa.jsonl", + protocol="http-json", + request_field="question", + response_fields={ + "response": "output.answer", + "context": "output.context", + "citations": "output.citations", + }, + ) + target = classify_agent(cfg.agent, cfg.protocol) + + def fake_request_json(**_kwargs): + return { + "output": { + "answer": "Use the reset page.", + "context": ["Password reset article"], + "citations": ["password.md"], + } + } + + monkeypatch.setattr(invocations, "_http_request_json", fake_request_json) + + result = invocations.invoke( + target, + cfg, + {"input": "How do I reset my password?"}, + timeout=1, + ) + + assert result.response == "Use the reset page." + assert result.metadata["response_fields"] == { + "response": "Use the reset page.", + "context": ["Password reset article"], + "citations": ["password.md"], + } + + +def test_response_fields_are_available_to_evaluator_mapping(monkeypatch) -> None: + captured: dict[str, object] = {} + + def fake_evaluator(**kwargs): + captured.update(kwargs) + return {"score": 5} + + cfg = AgentOpsConfig( + version=1, + agent="https://example.test/chat", + dataset="./qa.jsonl", + ) + target = classify_agent(cfg.agent, cfg.protocol) + monkeypatch.setattr( + orchestrator.invocations, + "invoke", + lambda *_args, **_kwargs: invocations.InvocationResult( + response="Use the reset page.", + latency_seconds=0.25, + metadata={ + "response_fields": { + "response": "Use the reset page.", + "context": ["Password reset article"], + } + }, + ), + ) + evaluator = runtime.EvaluatorRuntime( + preset=EvaluatorPreset( + name="groundedness", + class_name="GroundednessEvaluator", + score_key="groundedness", + input_mapping={ + "response": "$prediction", + "context": "$response.context", + }, + ), + callable=fake_evaluator, + ) + + row = orchestrator._evaluate_row( + row={"input": "question", "expected": "answer"}, + index=0, + total=1, + target=target, + config=cfg, + evaluators=[evaluator], + timeout=1, + progress=lambda _msg: None, + rules_by_metric={}, + ) + + assert row.response == "Use the reset page." + assert row.context == '["Password reset article"]' + assert captured == { + "response": "Use the reset page.", + "context": ["Password reset article"], + } diff --git a/tests/unit/test_runtime_conversation.py b/tests/unit/test_runtime_conversation.py index 605d2b54..a983e3fe 100644 --- a/tests/unit/test_runtime_conversation.py +++ b/tests/unit/test_runtime_conversation.py @@ -9,6 +9,7 @@ from __future__ import annotations from agentops.pipeline.runtime import _build_conversation_messages +from agentops.pipeline import runtime def test_builds_text_only_conversation_when_no_tool_calls() -> None: @@ -130,3 +131,27 @@ def test_skips_calls_without_a_name() -> None: # Only the named call survives, plus the final assistant text. assert len(out["response"]) == 2 assert out["response"][0]["content"][0]["name"] == "f" + + +def test_resolves_retrieval_and_telemetry_placeholders() -> None: + resolved = runtime._resolve_kwargs( + { + "context": "$retrieved_context", + "items": "$retrieved_context_items", + "trace": "$telemetry.trace_id", + "json_text": "$response.raw", + }, + row={ + "input": "q", + "retrieved_context": "doc text", + "retrieved_context_items": [{"id": "doc1"}], + "telemetry": {"trace_id": "trace-123"}, + "response": {"raw": "nested response"}, + }, + response="answer", + ) + + assert resolved["context"] == "doc text" + assert resolved["items"] == [{"id": "doc1"}] + assert resolved["trace"] == "trace-123" + assert resolved["json_text"] == "nested response" diff --git a/tests/unit/test_runtime_dataset_response_source.py b/tests/unit/test_runtime_dataset_response_source.py new file mode 100644 index 00000000..1680e4a2 --- /dev/null +++ b/tests/unit/test_runtime_dataset_response_source.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from agentops.core.agentops_config import AgentOpsConfig, classify_agent +from agentops.core.evaluators import EvaluatorPreset +from agentops.pipeline import orchestrator, runtime + + +def test_dataset_response_source_does_not_invoke_target(monkeypatch) -> None: + config = AgentOpsConfig( + version=1, + agent="https://example.test/chat", + dataset="./qa.jsonl", + response_source="dataset", + ) + target = classify_agent(config.agent, config.protocol) + latency = EvaluatorPreset( + name="avg_latency_seconds", + class_name="_latency", + score_key="avg_latency_seconds", + input_mapping={}, + ) + + def fail_invoke(*args, **kwargs): + raise AssertionError("target should not be invoked") + + monkeypatch.setattr(orchestrator.invocations, "invoke", fail_invoke) + + row = orchestrator._evaluate_row( + row={"input": "hello", "response": "cached answer", "expected": "cached answer"}, + index=0, + total=1, + target=target, + config=config, + evaluators=[runtime.load_evaluator(latency)], + timeout=1, + progress=lambda _msg: None, + rules_by_metric={}, + ) + + assert row.error is None + assert row.response == "cached answer" + assert row.latency_seconds == 0.0 + assert row.metrics[0].name == "avg_latency_seconds" + assert row.metrics[0].value == 0.0 + + +def test_dataset_response_source_accepts_prediction_field() -> None: + config = AgentOpsConfig( + version=1, + agent="https://example.test/chat", + dataset="./qa.jsonl", + response_source="dataset", + ) + target = classify_agent(config.agent, config.protocol) + latency = EvaluatorPreset( + name="avg_latency_seconds", + class_name="_latency", + score_key="avg_latency_seconds", + input_mapping={}, + ) + + row = orchestrator._evaluate_row( + row={"input": "hello", "prediction": "predicted answer"}, + index=0, + total=1, + target=target, + config=config, + evaluators=[runtime.load_evaluator(latency)], + timeout=1, + progress=lambda _msg: None, + rules_by_metric={}, + ) + + assert row.response == "predicted answer" diff --git a/tests/unit/test_telemetry_import.py b/tests/unit/test_telemetry_import.py new file mode 100644 index 00000000..aa07010f --- /dev/null +++ b/tests/unit/test_telemetry_import.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +import builtins +import json + +import pytest + +from agentops.core.agentops_config import AgentOpsConfig +from agentops.services.telemetry_import import ( + TelemetryImportError, + build_telemetry_kql, + find_telemetry_import, + query_azure_monitor, + transform_telemetry_rows, + write_telemetry_import, +) + + +def _config(**overrides): + data = { + "version": 1, + "agent": "support-agent:1", + "dataset": ".agentops/data/smoke.jsonl", + "telemetry_imports": [ + { + "name": "prod", + "target": "application-insights", + "resource_id": "$APPINSIGHTS_RESOURCE_ID", + "fields": { + "input": "customDimensions.question", + "response": "customDimensions.answer", + "context": "customDimensions.context", + }, + "output": {"path": ".agentops/data/prod.jsonl"}, + **overrides, + } + ], + } + return AgentOpsConfig.model_validate(data).telemetry_imports[0] + + +def test_transform_rows_dedupes_redacts_and_writes_manifest(tmp_path) -> None: + cfg = _config( + output={"path": str(tmp_path / "prod.jsonl")}, + privacy={"redact_fields": ["token"], "max_field_length": 100, "include_raw": True}, + ) + raw = [ + { + "operation_Id": "trace-1", + "id": "turn-1", + "customDimensions": { + "question": "How do I reset my password?", + "answer": "Open account settings.", + "context": "Reset article", + "token": "secret-token", + }, + }, + { + "operation_Id": "trace-1", + "id": "turn-1", + "customDimensions": { + "question": "How do I reset my password?", + "answer": "Open account settings.", + }, + }, + {"customDimensions": {"question": "missing response"}}, + ] + + preview = transform_telemetry_rows(cfg, raw) + write_telemetry_import(preview) + + assert len(preview.rows) == 1 + assert preview.deduped == 1 + assert preview.skipped == 1 + row = preview.rows[0] + assert row["input"] == "How do I reset my password?" + assert row["response"] == "Open account settings." + assert row["expected"] == "Open account settings." + assert row["context"] == "Reset article" + assert row["telemetry"]["trace_id"] == "trace-1" + assert row["raw"]["customDimensions"]["token"] == "[redacted]" + assert (tmp_path / "prod.jsonl").exists() + manifest = json.loads((tmp_path / "prod-manifest.json").read_text(encoding="utf-8")) + assert manifest["rows"] == 1 + assert manifest["deduped"] == 1 + + +def test_build_kql_uses_safe_generated_filters() -> None: + cfg = _config(filters={"customDimensions.agent": ["support", "sales"]}, max_rows=1000) + + kql = build_telemetry_kql(cfg, rows=5) + + assert "union isfuzzy=true requests, dependencies, traces" in kql + assert "| extend timestamp = coalesce(" in kql + assert "column_ifexists('timestamp', datetime(null))" in kql + assert "column_ifexists('TimeGenerated', datetime(null))" in kql + assert "coalesce(timestamp, TimeGenerated)" not in kql + assert "ago(7d)" in kql + assert ( + "tostring(column_ifexists('customDimensions', dynamic({}))['agent']) " + "in ('support', 'sales')" + ) in kql + assert "operation_Id = column_ifexists('operation_Id', '')" in kql + assert "TimeGenerated =" not in kql + assert "| order by timestamp desc" in kql + assert "take 5" in kql + + +def test_build_kql_guards_plain_filter_columns() -> None: + cfg = _config(filters={"name": "agent.response"}) + + kql = build_telemetry_kql(cfg, rows=10) + + assert "tostring(column_ifexists('name', '')) == 'agent.response'" in kql + assert "tostring(name)" not in kql + + +def test_build_kql_rejects_unsafe_filter_field() -> None: + cfg = _config(filters={"name); drop table traces; //": "x"}) + + with pytest.raises(TelemetryImportError, match="unsafe"): + build_telemetry_kql(cfg) + + +def test_find_telemetry_import_reports_available_names() -> None: + cfg = AgentOpsConfig.model_validate( + { + "version": 1, + "agent": "support-agent:1", + "dataset": ".agentops/data/smoke.jsonl", + "telemetry_imports": [ + {"name": "prod", "target": "log-analytics", "workspace_id": "workspace"} + ], + } + ) + + with pytest.raises(TelemetryImportError, match="prod"): + find_telemetry_import(cfg, "missing") + + +def test_query_azure_monitor_reports_missing_sdk(monkeypatch) -> None: + cfg = _config() + original_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "azure.identity": + raise ImportError("no azure") + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", fake_import) + + with pytest.raises(TelemetryImportError, match="azure-identity"): + query_azure_monitor(cfg, rows=1) From 8675084c20b47e6ce48ff479a0fd0a16da49a4db Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:53:26 +0000 Subject: [PATCH 7/9] chore: prepare release 0.6.0 --- .claude-plugin/marketplace.json | 2 +- .github/plugin/marketplace.json | 2 +- CHANGELOG.md | 2 ++ plugins/agentops/package.json | 2 +- plugins/agentops/plugin.json | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bf525cc7..c26e6640 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "agentops-accelerator", "source": "../../plugins/agentops", "description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Toolkit and Microsoft Foundry agents.", - "version": "0.3.23", + "version": "0.6.0", "keywords": [ "agentops", "evaluation", diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index bf525cc7..c26e6640 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "agentops-accelerator", "source": "../../plugins/agentops", "description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Toolkit and Microsoft Foundry agents.", - "version": "0.3.23", + "version": "0.6.0", "keywords": [ "agentops", "evaluation", diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb264ae..c4e7dc0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ This format follows [Keep a Changelog](https://keepachangelog.com/) and adheres ## [Unreleased] +## [0.6.0] - 2026-06-26 + ### Added - **Retrieval telemetry can now be imported as evaluation datasets.** The new `telemetry_imports` config contract and `agentops telemetry validate`, diff --git a/plugins/agentops/package.json b/plugins/agentops/package.json index 79f710c6..725073fd 100644 --- a/plugins/agentops/package.json +++ b/plugins/agentops/package.json @@ -2,7 +2,7 @@ "name": "agentops-accelerator", "displayName": "AgentOps Accelerator — Skills for GitHub Copilot", "description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Accelerator and Microsoft Foundry agents.", - "version": "0.3.23", + "version": "0.6.0", "publisher": "AgentOpsAccelerator", "icon": "icon.png", "license": "MIT", diff --git a/plugins/agentops/plugin.json b/plugins/agentops/plugin.json index 73bdffbb..a40491af 100644 --- a/plugins/agentops/plugin.json +++ b/plugins/agentops/plugin.json @@ -1,7 +1,7 @@ { "name": "agentops-accelerator", "description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Accelerator and Microsoft Foundry agents.", - "version": "0.3.23", + "version": "0.6.0", "author": { "name": "AgentOps Accelerator", "url": "https://github.com/Azure/agentops" From bf1e6abc69b2c050aa182d00f5834cab22f1e205 Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Fri, 26 Jun 2026 14:14:52 -0300 Subject: [PATCH 8/9] Fix v0.6.0 release build and evaluator ToC Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ndry-evaluation-sdk-built-in-evaluators.md | 26 ++++++++++++++----- .../skills/agentops-workflow/SKILL.md | 17 ++++++------ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/docs/foundry-evaluation-sdk-built-in-evaluators.md b/docs/foundry-evaluation-sdk-built-in-evaluators.md index e6cba1ad..41f1004a 100644 --- a/docs/foundry-evaluation-sdk-built-in-evaluators.md +++ b/docs/foundry-evaluation-sdk-built-in-evaluators.md @@ -39,7 +39,7 @@ evaluators: - RelevanceEvaluator ``` -## Evaluator families +## Requirements | Family | What it checks | Common inputs | |---|---|---| @@ -49,7 +49,7 @@ evaluators: | Agent judges | Tool use and agent workflow behavior are correct. | prompt, response, tool calls, tool definitions | | Local metrics | Scores that do not need a judge model. | response, expected answer, latency | -## Evaluator inputs +## Parameters AgentOps uses a small set of logical inputs. The same logical input can come from a static dataset, a live HTTP response, or imported telemetry. @@ -65,7 +65,7 @@ a static dataset, a live HTTP response, or imported telemetry. | `tool_definitions` | Tool schemas available to the agent. | dataset row | | `trace_id` | Trace lineage for review and troubleshooting. | `$telemetry.trace_id` | -## Mapping rules +## Rules The mapping rules are intentionally boring: @@ -120,7 +120,18 @@ agentops telemetry preview prod-rag --rows 10 agentops telemetry import prod-rag --apply ``` -## Quality judges +When comparing this page with raw SDK examples, use these mappings: + +- Quality evaluators often show `model_config`. In AgentOps, set the judge model + with `AZURE_OPENAI_DEPLOYMENT` or `AZURE_AI_MODEL_DEPLOYMENT_NAME`. +- Safety evaluators often show `azure_ai_project`. In AgentOps, set the Foundry + project with `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` or `project_endpoint:`. +- Agent evaluators need the agent payload to include tool calls and tool + definitions when you want tools to be judged. +- NLP metrics are non-LLM checks over values such as `response` and + `ground_truth`. + +## Quality | Evaluator | Typical inputs | Notes | |---|---|---| @@ -134,7 +145,7 @@ Quality judges need a judge model deployment. Set `AZURE_OPENAI_DEPLOYMENT` or `AZURE_AI_MODEL_DEPLOYMENT_NAME` when local or cloud evaluation needs one. -## Safety judges +## Safety | Evaluator | Typical inputs | Notes | |---|---|---| @@ -148,7 +159,7 @@ cloud evaluation needs one. Safety judges require a Foundry project connection. Use `AZURE_AI_FOUNDRY_PROJECT_ENDPOINT` or `project_endpoint:` in `agentops.yaml`. -## Agent judges +## Agent | Evaluator | Typical inputs | Notes | |---|---|---| @@ -163,7 +174,7 @@ Agent judges work best when the target returns tool telemetry or the dataset row contains expected tool calls. If the endpoint cannot expose tool calls, start with answer quality and RAG judges instead. -## Local metrics +## NLP | Evaluator | Typical inputs | Notes | |---|---|---| @@ -197,3 +208,4 @@ AgentOps keeps cloud evaluation setup minimal: - Production trace imports need review before they become blocking release gates. **Last updated:** 2026-06-26 (UTC) + diff --git a/plugins/agentops/skills/agentops-workflow/SKILL.md b/plugins/agentops/skills/agentops-workflow/SKILL.md index 90cb635b..39cba197 100644 --- a/plugins/agentops/skills/agentops-workflow/SKILL.md +++ b/plugins/agentops/skills/agentops-workflow/SKILL.md @@ -62,11 +62,11 @@ by discovering the whole Azure subscription. - `azd env get-values` when `azure.yaml` exists and azd is available. - `.github/workflows/agentops-*.yml`. 2. Read the generated workflows to determine exactly which GitHub environments - and variables are needed. For the prompt-agent tutorial, `pr` normally - means only `environment: dev`. -3. Treat `dev` here as a GitHub Actions environment for OIDC and variables. It - normally points at the Foundry project already configured by `agentops init`; - it does not require creating a new Foundry project. + and variables are needed. For prompt-agent PR gates, `pr` uses + `environment: sandbox`; deploy workflows use `dev`, `qa`, or `production`. +3. Treat these as GitHub Actions environments for OIDC and variables. `sandbox` + points at the Foundry authoring project. `dev` points at the first shared + post-merge project. 4. Proceed only when these values are known or deliberately chosen: - GitHub `owner/repo`. - workflow environment names from `jobs.*.environment`. @@ -271,7 +271,7 @@ The full scaffold writes: | Kind | GitHub Actions path | Azure DevOps path | Trigger | Environment | |---|---|---|---|---| -| `pr` | `.github/workflows/agentops-pr.yml` | `.azuredevops/pipelines/agentops-pr.yml` | PRs to `develop`, `release/**`, `main` | `dev` | +| `pr` | `.github/workflows/agentops-pr.yml` | `.azuredevops/pipelines/agentops-pr.yml` | PRs to `develop`, `release/**`, `main` | `sandbox` for prompt-agent PR candidates, `dev` for generic PR gates | | `dev` | `.github/workflows/agentops-deploy-dev.yml` | `.azuredevops/pipelines/agentops-deploy-dev.yml` | push to `develop` | `dev` | | `qa` | `.github/workflows/agentops-deploy-qa.yml` | `.azuredevops/pipelines/agentops-deploy-qa.yml` | push to `release/**` | `qa` | | `prod` | `.github/workflows/agentops-deploy-prod.yml` | `.azuredevops/pipelines/agentops-deploy-prod.yml` | push to `main` | `production` | @@ -303,9 +303,10 @@ Useful flags: ### GitHub Actions Read the generated workflow files and create only the GitHub Environments used -by `jobs.*.environment`. For `pr`, that is usually only **`dev`**. For the full -scaffold, create **`dev`**, **`qa`**, and **`production`**. +by `jobs.*.environment`. For prompt-agent PR gates, create **`sandbox`**. For the +full scaffold, create **`sandbox`**, **`dev`**, **`qa`**, and **`production`**. +- **`sandbox`** - no extra protection. Store the PR candidate endpoint and OIDC variables here when generated jobs use `environment: sandbox`. - **`dev`** - no extra protection. Store the OIDC variables here when the generated jobs use `environment: dev`. - **`qa`** - usually no required reviewers, but isolated variables for QA. From 6fd7ee45f458538aa695fc6e987f01dd90dfc6c3 Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Fri, 26 Jun 2026 14:15:13 -0300 Subject: [PATCH 9/9] Remove release doc trailing whitespace Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/foundry-evaluation-sdk-built-in-evaluators.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/foundry-evaluation-sdk-built-in-evaluators.md b/docs/foundry-evaluation-sdk-built-in-evaluators.md index 41f1004a..5fb77230 100644 --- a/docs/foundry-evaluation-sdk-built-in-evaluators.md +++ b/docs/foundry-evaluation-sdk-built-in-evaluators.md @@ -208,4 +208,3 @@ AgentOps keeps cloud evaluation setup minimal: - Production trace imports need review before they become blocking release gates. **Last updated:** 2026-06-26 (UTC) -