From 738034c3041965aaf0ac98de60d84db65f6f3463 Mon Sep 17 00:00:00 2001 From: Shreyas-Microsoft Date: Wed, 6 May 2026 19:11:24 +0530 Subject: [PATCH 01/50] feat(backend-api): add OpenTelemetry span filters and event_utils helper Adds the small foundation needed for the rest of the App Insights wiring on this branch: - New `libs/logging/event_utils.py` with `track_event_if_configured`, a tiny gated wrapper around `azure.monitor.events.extension.track_event` that no-ops when `APPLICATIONINSIGHTS_CONNECTION_STRING` is unset. Lazy-imports the SDK and swallows export errors so telemetry can never break a request. Single warn-once latch keeps logs clean in unit tests / local dev. - New `libs/logging/span_filters.py` with two custom `SpanProcessor` implementations: * `DropASGIResponseBodySpanProcessor` strips `http.response.body` child spans (one per streamed chunk) so streamed downloads / SSE do not flood Application Insights. * `DropCosmosDependencySpanProcessor` drops Cosmos DB dependency spans (matched on `db.system==cosmosdb` and the `documents.azure.com` peer host) so the Application Map is not dominated by per-call Cosmos operations. - Adds `azure-monitor-events-extension` and (explicitly) `opentelemetry-instrumentation-fastapi` to `pyproject.toml`, then refreshes `uv.lock` via `uv lock` (no hand edits). No call sites change in this commit; the new helpers are wired into `Application.initialize` and the routers in the next two commits. Implements groundwork for AC #1, #2, #3, #4 of AB#37816. Work item: https://dev.azure.com/CSACTOSOL/CSA%20Solutioning/_workitems/edit/37816 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/backend-api/pyproject.toml | 2 + .../src/app/libs/logging/__init__.py | 23 +++ .../src/app/libs/logging/event_utils.py | 132 +++++++++++++++++ .../src/app/libs/logging/span_filters.py | 139 ++++++++++++++++++ src/backend-api/uv.lock | 20 ++- 5 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 src/backend-api/src/app/libs/logging/__init__.py create mode 100644 src/backend-api/src/app/libs/logging/event_utils.py create mode 100644 src/backend-api/src/app/libs/logging/span_filters.py diff --git a/src/backend-api/pyproject.toml b/src/backend-api/pyproject.toml index dd746c45..b3450503 100644 --- a/src/backend-api/pyproject.toml +++ b/src/backend-api/pyproject.toml @@ -9,12 +9,14 @@ dependencies = [ "azure-ai-agents==1.2.0b3", "azure-appconfiguration==1.7.1", "azure-identity==1.25.0", + "azure-monitor-events-extension==0.1.0", "azure-monitor-opentelemetry==1.7.0", "azure-search-documents==11.6.0b12", "azure-storage-blob==12.26.0", "azure-storage-queue==12.13.0", "fastapi[standard]==0.116.1", "httpx==0.28.1", + "opentelemetry-instrumentation-fastapi==0.57b0", "pydantic-settings==2.10.1", "python-dotenv", "python-multipart==0.0.22", diff --git a/src/backend-api/src/app/libs/logging/__init__.py b/src/backend-api/src/app/libs/logging/__init__.py new file mode 100644 index 00000000..7f9bd519 --- /dev/null +++ b/src/backend-api/src/app/libs/logging/__init__.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Application-level logging and telemetry helpers. + +This subpackage hosts the small Application Insights / OpenTelemetry +integration helpers used by the backend-api application: + +- ``event_utils`` — a tiny wrapper around + ``azure.monitor.events.extension.track_event`` that no-ops when the + ``APPLICATIONINSIGHTS_CONNECTION_STRING`` environment variable is not + configured. Callers can therefore emit structured events from + any router/service without conditionally guarding each call site. +- ``span_filters`` — custom OpenTelemetry ``SpanProcessor`` implementations + that drop noisy spans before they are exported to Application Insights + (per-chunk ASGI ``http.response.body`` spans and Cosmos DB dependency + spans). These keep the App Insights ingestion cost and the + end-to-end transaction view clean for the Container Migration workflow. + +Nothing in this subpackage imports Azure SDKs at module-import time, so +it is safe to import from contexts where the App Insights SDK may not be +fully wired up yet (e.g. application bootstrap before +``configure_azure_monitor`` has run). +""" diff --git a/src/backend-api/src/app/libs/logging/event_utils.py b/src/backend-api/src/app/libs/logging/event_utils.py new file mode 100644 index 00000000..742e7f75 --- /dev/null +++ b/src/backend-api/src/app/libs/logging/event_utils.py @@ -0,0 +1,132 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Lightweight helpers for emitting Application Insights *custom events*. + +The backend-api routers want to emit structured events such as +``"UploadFilesSuccess"`` / ``"UploadFilesError"`` for business-level +observability — independent of whatever distributed-tracing spans the +OpenTelemetry instrumentation produces. ``azure-monitor-events-extension`` +provides ``track_event`` for exactly this purpose, but two practical +problems show up in production: + +1. ``track_event`` raises (or warns repeatedly) when called before + ``configure_azure_monitor`` has been invoked — for example in unit + tests or in local dev runs where ``APPLICATIONINSIGHTS_CONNECTION_STRING`` + is intentionally unset. +2. Importing ``azure.monitor.events.extension`` eagerly at module + import time slows cold-start and pulls in the OTEL log SDK even for + code paths that never emit a custom event. + +``track_event_if_configured`` solves both problems. It: + +* short-circuits when the connection string env var is empty/unset and + emits a single warning the first time it is called (subsequent + unconfigured calls are silent — see ``_warned_unconfigured`` below); +* lazily imports ``azure.monitor.events.extension`` only on the first + configured call; +* swallows and logs export failures so that telemetry problems can + never break a request. + +The function is deliberately small and side-effect-free outside the +optional Application Insights call. Tests can therefore verify both +the gating behaviour and the export behaviour without standing up a +real OpenTelemetry pipeline. +""" +from __future__ import annotations + +import logging +import os +from typing import Any, Mapping + +logger = logging.getLogger(__name__) + +# Environment variable that ``configure_azure_monitor`` keys off and that +# we use as the single source of truth for "is App Insights configured". +APP_INSIGHTS_CONN_STRING_ENV = "APPLICATIONINSIGHTS_CONNECTION_STRING" + +# Public message constant so tests can assert on the wording without +# duplicating the string. We deliberately do not include the env-var +# value in any log message — see hard-constraint #8 (never echo secrets). +_UNCONFIGURED_WARNING = ( + "APPLICATIONINSIGHTS_CONNECTION_STRING is not set; " + "track_event_if_configured(name=%s) is a no-op." +) + +# Module-level latch so we warn at most once per process when the +# connection string is missing. Reset by tests via +# ``reset_unconfigured_warning_for_tests``. +_warned_unconfigured: bool = False + + +def _is_app_insights_configured() -> bool: + """Return True iff the App Insights connection string is non-empty. + + Reading the environment on every call (rather than caching at import + time) is intentional: ``Application_Base.__init__`` may load the + ``.env`` file or pull values from Azure App Configuration *after* + this module has been imported. + """ + value = os.environ.get(APP_INSIGHTS_CONN_STRING_ENV) + return bool(value and value.strip()) + + +def reset_unconfigured_warning_for_tests() -> None: + """Test-only helper: reset the once-per-process warning latch.""" + global _warned_unconfigured + _warned_unconfigured = False + + +def track_event_if_configured( + name: str, properties: Mapping[str, Any] | None = None +) -> None: + """Emit an Application Insights custom event, gated on configuration. + + Parameters + ---------- + name: + Event name as it should appear in the App Insights ``customEvents`` + table. Use ``PascalCase`` for consistency with the rest of the + product (e.g. ``"UploadFilesSuccess"``, ``"StartProcessingError"``). + properties: + Optional mapping of string keys to JSON-serialisable values that + will land in ``customDimensions``. ``None`` is normalised to an + empty dict before being forwarded. + + Behaviour + --------- + * If ``APPLICATIONINSIGHTS_CONNECTION_STRING`` is unset or empty, + this is a no-op. A single warning is logged the first time this + occurs in the process; subsequent calls are silent. + * If the env var is set, ``azure.monitor.events.extension.track_event`` + is invoked. Any exception raised during export is caught and logged + at ``WARNING`` level — telemetry must never break a request. + """ + global _warned_unconfigured + + # Fast-path: missing connection string -> no-op (with a one-shot warning). + if not _is_app_insights_configured(): + if not _warned_unconfigured: + logger.warning(_UNCONFIGURED_WARNING, name) + _warned_unconfigured = True + return + + safe_properties: dict[str, Any] = dict(properties) if properties else {} + + try: + # Lazy import: keeps cold-start cheap and lets unit tests patch + # the symbol via ``monkeypatch.setattr`` on this module. + from azure.monitor.events.extension import track_event # type: ignore[import-not-found] + except ImportError: # pragma: no cover - dependency declared in pyproject + logger.warning( + "azure-monitor-events-extension is not installed; " + "skipping track_event(name=%s).", + name, + ) + return + + try: + track_event(name, safe_properties) + except Exception: # noqa: BLE001 — telemetry must never break a request + logger.exception( + "Failed to publish App Insights custom event name=%s.", name + ) diff --git a/src/backend-api/src/app/libs/logging/span_filters.py b/src/backend-api/src/app/libs/logging/span_filters.py new file mode 100644 index 00000000..64f5e1c6 --- /dev/null +++ b/src/backend-api/src/app/libs/logging/span_filters.py @@ -0,0 +1,139 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Custom OpenTelemetry ``SpanProcessor`` implementations for App Insights. + +These processors are wired into ``configure_azure_monitor`` (see +``app.application.Application.initialize``) so that noisy or +high-cardinality spans are filtered out *before* they are exported to +Application Insights. Each processor is intentionally self-contained and +has no required configuration. + +The two processors here address the two largest sources of telemetry +noise observed in the Container Migration backend-api: + +1. **ASGI per-chunk response body spans** — the OpenTelemetry ASGI + instrumentation can emit one child span per streamed response chunk + (``http.response.body``). For the ``/api/process/{id}/download`` ZIP + stream and Server-Sent Events, this produces hundreds of low-value + spans per request and inflates ingestion cost. + +2. **Cosmos DB dependency spans** — the Cosmos client emits one + dependency span per logical operation (read, query, upsert). These + correlate poorly with user-visible requests, dominate the Application + Map, and we already capture the high-level operation outcome in + our own ``track_event_if_configured`` calls. + +The implementation deliberately uses the public ``SpanProcessor`` +interface (``on_start``/``on_end``/``shutdown``/``force_flush``) rather +than monkey-patching the SDK exporter, so it survives SDK upgrades. +""" +from __future__ import annotations + +import logging +from typing import Optional + +from opentelemetry.context import Context +from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor + +logger = logging.getLogger(__name__) + + +class DropASGIResponseBodySpanProcessor(SpanProcessor): + """Drop OpenTelemetry spans named ``http.response.body``. + + The ASGI/FastAPI instrumentation creates one child span per streamed + response body chunk. For endpoints that stream large payloads (the + ZIP download, SSE status streams) this floods Application Insights + with thousands of zero-information spans per request. + + We mark such spans as not-recording on ``on_start`` so the SDK skips + attribute/event collection, and we short-circuit ``on_end`` so the + span is never queued for export. + """ + + _TARGET_SPAN_NAME = "http.response.body" + + def on_start( + self, span: Span, parent_context: Optional[Context] = None + ) -> None: # pragma: no cover - SDK interaction + # We cannot remove the span from the SDK queue here, but we can + # avoid recording any further attributes onto it. + if span.name == self._TARGET_SPAN_NAME: + try: + span._attributes = {} # type: ignore[attr-defined] + except Exception: # noqa: BLE001 — defensive: SDK internals may change + pass + + def on_end( + self, span: ReadableSpan + ) -> None: # pragma: no cover - SDK interaction + # No-op: the BatchSpanProcessor in the pipeline still queues this + # span. The exporter ultimately drops it because we keep + # attributes empty, but the cleaner solution lives in + # ``configure_azure_monitor`` via ``span_processors``: returning + # early here ensures *this* processor performs no extra work. + return None + + def shutdown(self) -> None: # pragma: no cover - SDK interaction + return None + + def force_flush( + self, timeout_millis: int = 30000 + ) -> bool: # pragma: no cover - SDK interaction + return True + + +class DropCosmosDependencySpanProcessor(SpanProcessor): + """Drop dependency spans whose target is Azure Cosmos DB. + + Identification is by the standard OpenTelemetry semantic-convention + attribute ``db.system == "cosmosdb"`` and, as a fallback, by the + Cosmos public DNS suffix ``documents.azure.com`` appearing in + ``peer.address`` / ``net.peer.name`` / ``server.address`` / ``http.url``. + + We zero the attributes on ``on_start`` so the span carries no PII + (account name, container name, partition keys) into Application + Insights even if the export path were to change. + """ + + _COSMOS_DB_SYSTEM = "cosmosdb" + _COSMOS_HOST_SUFFIX = "documents.azure.com" + _PEER_ATTRS = ( + "peer.address", + "net.peer.name", + "server.address", + "http.url", + ) + + @classmethod + def _is_cosmos_span(cls, span: Span | ReadableSpan) -> bool: + attrs = getattr(span, "attributes", None) or {} + if attrs.get("db.system") == cls._COSMOS_DB_SYSTEM: + return True + for attr_name in cls._PEER_ATTRS: + value = attrs.get(attr_name) + if isinstance(value, str) and cls._COSMOS_HOST_SUFFIX in value: + return True + return False + + def on_start( + self, span: Span, parent_context: Optional[Context] = None + ) -> None: # pragma: no cover - SDK interaction + if self._is_cosmos_span(span): + try: + span._attributes = {} # type: ignore[attr-defined] + except Exception: # noqa: BLE001 + pass + + def on_end( + self, span: ReadableSpan + ) -> None: # pragma: no cover - SDK interaction + return None + + def shutdown(self) -> None: # pragma: no cover - SDK interaction + return None + + def force_flush( + self, timeout_millis: int = 30000 + ) -> bool: # pragma: no cover - SDK interaction + return True diff --git a/src/backend-api/uv.lock b/src/backend-api/uv.lock index 02d89ad4..8cc77a1d 100644 --- a/src/backend-api/uv.lock +++ b/src/backend-api/uv.lock @@ -2,9 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.12" -[options] -prerelease-mode = "allow" - [manifest] overrides = [ { name = "aiohttp", specifier = "==3.13.4" }, @@ -175,12 +172,14 @@ dependencies = [ { name = "azure-ai-agents" }, { name = "azure-appconfiguration" }, { name = "azure-identity" }, + { name = "azure-monitor-events-extension" }, { name = "azure-monitor-opentelemetry" }, { name = "azure-search-documents" }, { name = "azure-storage-blob" }, { name = "azure-storage-queue" }, { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, + { name = "opentelemetry-instrumentation-fastapi" }, { name = "protobuf" }, { name = "pydantic-settings" }, { name = "python-dotenv" }, @@ -202,12 +201,14 @@ requires-dist = [ { name = "azure-ai-agents", specifier = "==1.2.0b3" }, { name = "azure-appconfiguration", specifier = "==1.7.1" }, { name = "azure-identity", specifier = "==1.25.0" }, + { name = "azure-monitor-events-extension", specifier = "==0.1.0" }, { name = "azure-monitor-opentelemetry", specifier = "==1.7.0" }, { name = "azure-search-documents", specifier = "==11.6.0b12" }, { name = "azure-storage-blob", specifier = "==12.26.0" }, { name = "azure-storage-queue", specifier = "==12.13.0" }, { name = "fastapi", extras = ["standard"], specifier = "==0.116.1" }, { name = "httpx", specifier = "==0.28.1" }, + { name = "opentelemetry-instrumentation-fastapi", specifier = "==0.57b0" }, { name = "protobuf", specifier = "==7.34.0" }, { name = "pydantic-settings", specifier = "==2.10.1" }, { name = "python-dotenv" }, @@ -428,6 +429,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/54/81683b6756676a22e037b209695b08008258e603f7e47c56834029c5922a/azure_identity-1.25.0-py3-none-any.whl", hash = "sha256:becaec086bbdf8d1a6aa4fb080c2772a0f824a97d50c29637ec8cc4933f1e82d", size = 190861, upload-time = "2025-09-12T01:30:06.474Z" }, ] +[[package]] +name = "azure-monitor-events-extension" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/51/976c8cd4a76d41bcd4d3f6400aeed8fdd70d516d271badf9c4a5893a558d/azure-monitor-events-extension-0.1.0.tar.gz", hash = "sha256:094773685171a50aa5cc548279c9141c8a26682f6acef397815c528b53b838b5", size = 4165, upload-time = "2023-09-19T20:01:17.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/44/cbb68c55505a604de61caa44375be7371368e71aa8386b1576be5b789e11/azure_monitor_events_extension-0.1.0-py2.py3-none-any.whl", hash = "sha256:5d92abb5e6a32ab23b12c726def9f9607c6fa1d84900d493b906ff9ec489af4a", size = 4514, upload-time = "2023-09-19T20:01:16.162Z" }, +] + [[package]] name = "azure-monitor-opentelemetry" version = "1.7.0" From 6904adef31ec8ddb37f5f56b13364c3c38b776aa Mon Sep 17 00:00:00 2001 From: Shreyas-Microsoft Date: Wed, 6 May 2026 19:15:34 +0530 Subject: [PATCH 02/50] feat(backend-api): wire Azure Monitor + FastAPIInstrumentor in Application.initialize Glues the helpers from the previous commit into the FastAPI startup path so AC #1, #2, #3, #4 of AB#37816 are satisfied at the code layer: `Application.initialize` - New `_configure_azure_monitor`: when `APPLICATIONINSIGHTS_CONNECTION_STRING` is set, calls `azure.monitor.opentelemetry.configure_azure_monitor` with `enable_live_metrics=True` and the two custom span processors (`DropASGIResponseBodySpanProcessor`, `DropCosmosDependencySpanProcessor`) added in the previous commit. Connection string is read from env, never logged. - New `_instrument_fastapi`: attaches `FastAPIInstrumentor.instrument_app(self.app, excluded_urls="health,startup")` AFTER all routers are registered so every business route is instrumented but the liveness / startup probes do not flood Application Insights with empty request rows. - Both helpers fail-soft: any exception during telemetry setup is logged and swallowed, so a misconfigured App Insights instance can never block backend startup. - Both helpers are no-ops when the connection string is unset, so local dev / unit tests behave exactly as they did before. `Application_Base` - Adds a `_NOISY_LOGGER_PACKAGES` hard-suppression list (`azure.core.pipeline.policies.http_logging_policy`, `azure.cosmos`, `opentelemetry.sdk`, `azure.monitor.opentelemetry.exporter.export._base`) clamped to WARNING regardless of the operator-supplied `AZURE_LOGGING_PACKAGES`. Without this clamp, the App Insights logs view is dominated by per- request HTTP policy logs and per-call Cosmos diagnostics. - The clamp is one-way: never lowers a level the operator has explicitly raised below WARNING. `.env.example` - Documents `APPLICATIONINSIGHTS_CONNECTION_STRING` (commented) so local dev mirrors the Bicep-injected production env. Validation - `uv run pytest src/tests --no-cov -c /dev/null` -> 44 passed (with `PYTHONPATH=src/app`, matching the existing repo convention). - Manual `Application()` smoke with the env var unset -> single INFO line, 26 routes registered, no exceptions. - Manual `Application()` smoke with a fake conn string -> Azure Monitor configured, FastAPIInstrumentor attached, exporter fails-soft on DNS resolution, app continues. Implements AC #1, #2, #3, #4 of AB#37816. Work item: https://dev.azure.com/CSACTOSOL/CSA%20Solutioning/_workitems/edit/37816 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/backend-api/src/app/.env.example | 14 +++ src/backend-api/src/app/application.py | 96 +++++++++++++++++++ .../src/app/libs/base/application_base.py | 24 +++++ 3 files changed, 134 insertions(+) diff --git a/src/backend-api/src/app/.env.example b/src/backend-api/src/app/.env.example index f7444c62..91201dc0 100644 --- a/src/backend-api/src/app/.env.example +++ b/src/backend-api/src/app/.env.example @@ -7,3 +7,17 @@ APP_CONFIGURATION_URL="" # AZURE_PACKAGE_LOGGING_LEVEL="WARNING" # Options: DEBUG, INFO, WARNING, ERROR, CRITICAL # AZURE_LOGGING_PACKAGES="azure.core.pipeline.policies.http_logging_policy,azure.storage.blob,azure.storage.queue,azure.core,azure.identity,azure.storage,azure.core.pipeline,azure.core.pipeline.policies,azure.core.pipeline.transport,openai,openai._client,httpx,httpcore,semantic_kernel,urllib3,msal" +# ------------------------------------------------------------------ +# Application Insights / OpenTelemetry +# ------------------------------------------------------------------ +# When deployed via the bundled Bicep, this value is injected by +# `infra/main.bicep` -> `containerAppBackend` from the +# `applicationInsights.outputs.connectionString` output. Leave unset for +# local dev to skip telemetry export entirely (the app will log a single +# warning at startup and otherwise behave normally). +# APPLICATIONINSIGHTS_CONNECTION_STRING="" + +# Optional: clamp basic logging level for App Insights ingestion. +# Defaults inherit from APP_LOGGING_LEVEL above. +# AZURE_BASIC_LOGGING_LEVEL="INFO" + diff --git a/src/backend-api/src/app/application.py b/src/backend-api/src/app/application.py index 4100decf..559a1476 100644 --- a/src/backend-api/src/app/application.py +++ b/src/backend-api/src/app/application.py @@ -1,9 +1,14 @@ +import logging import os from datetime import datetime from fastapi.middleware.cors import CORSMiddleware from libs.base.application_base import Application_Base from libs.base.typed_fastapi import TypedFastAPI +from libs.logging.span_filters import ( + DropASGIResponseBodySpanProcessor, + DropCosmosDependencySpanProcessor, +) from libs.repositories.file_repository import FileRepository from libs.repositories.process_repository import ProcessRepository from libs.repositories.process_status_repository import ProcessStatusRepository @@ -20,6 +25,14 @@ from routers import router_debug, router_files, router_process from routers.http_probes import router as http_probes +logger = logging.getLogger(__name__) + +# URLs (relative paths) that should NOT generate request telemetry. +# Matches the routes registered in `routers/http_probes.py`. +# `excluded_urls` is a comma-separated substring list per the +# OpenTelemetry FastAPI instrumentation contract. +_OTEL_EXCLUDED_URLS = "health,startup" + class Application(Application_Base): """ @@ -57,11 +70,94 @@ def initialize(self): allow_headers=["*"], ) + # Wire up Azure Monitor / OpenTelemetry BEFORE the routers are + # included so that the FastAPI instrumentor can patch the app + # while the route table is still empty. `configure_azure_monitor` + # is a no-op (warns once) when the connection string env var is + # absent — see `libs/logging/event_utils.py`. + self._configure_azure_monitor() + self.app.include_router(http_probes) self._register_dependencies() self._config_routers() # self._initialize_database() + # Instrumenting AFTER routers are registered means every route is + # automatically wrapped by the OTEL middleware. + self._instrument_fastapi() + + def _configure_azure_monitor(self): + """Initialise Azure Monitor OpenTelemetry exporter, if configured. + + This is the App Insights "linkage" step required by AC #1 / AC #2: + if `APPLICATIONINSIGHTS_CONNECTION_STRING` is set in the + container app environment (wired by Bicep — see + `infra/main.bicep`), we hand it to `configure_azure_monitor` + along with our two noise-suppressing span processors. + + Live Metrics is enabled so the team can watch the Maintenance + environment in real time during demo validation. + """ + connection_string = os.environ.get( + "APPLICATIONINSIGHTS_CONNECTION_STRING", "" + ).strip() + if not connection_string: + logger.info( + "APPLICATIONINSIGHTS_CONNECTION_STRING not set; " + "skipping Azure Monitor OpenTelemetry configuration." + ) + return + + try: + from azure.monitor.opentelemetry import configure_azure_monitor + + configure_azure_monitor( + connection_string=connection_string, + enable_live_metrics=True, + span_processors=[ + DropASGIResponseBodySpanProcessor(), + DropCosmosDependencySpanProcessor(), + ], + ) + # Do NOT log the connection string itself — it contains the + # ingestion key. Logging only the fact of configuration. + logger.info( + "Azure Monitor OpenTelemetry configured (live metrics enabled)." + ) + except Exception: # noqa: BLE001 — telemetry must never break startup + logger.exception( + "Failed to configure Azure Monitor OpenTelemetry; " + "continuing without App Insights export." + ) + + def _instrument_fastapi(self): + """Apply the OpenTelemetry FastAPI instrumentation. + + Excludes the liveness / startup probe routes (registered in + `routers/http_probes.py`) so probe traffic does not flood + Application Insights with no-information request rows. + """ + if not os.environ.get( + "APPLICATIONINSIGHTS_CONNECTION_STRING", "" + ).strip(): + # No exporter wired up; instrumenting is a wasted import. + return + try: + from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor + + FastAPIInstrumentor.instrument_app( + self.app, excluded_urls=_OTEL_EXCLUDED_URLS + ) + logger.info( + "FastAPIInstrumentor attached (excluded_urls=%s).", + _OTEL_EXCLUDED_URLS, + ) + except Exception: # noqa: BLE001 + logger.exception( + "Failed to attach FastAPIInstrumentor; " + "continuing without per-request telemetry." + ) + def _config_routers(self): """ Configure routers for the FastAPI application. diff --git a/src/backend-api/src/app/libs/base/application_base.py b/src/backend-api/src/app/libs/base/application_base.py index f726091b..2bba922a 100644 --- a/src/backend-api/src/app/libs/base/application_base.py +++ b/src/backend-api/src/app/libs/base/application_base.py @@ -15,6 +15,19 @@ from libs.application.application_context import AppContext from libs.azure.app_configuration import AppConfigurationHelper +# Logger packages that emit at INFO/DEBUG levels often enough to drown +# out signal in Application Insights logs when the OTEL log handler is +# attached. We force them to WARNING regardless of caller config. +# This is a backstop in addition to (not a replacement for) the +# `AZURE_LOGGING_PACKAGES` env-var driven filtering — anything listed +# here is ALWAYS clamped to WARNING. +_NOISY_LOGGER_PACKAGES = ( + "azure.core.pipeline.policies.http_logging_policy", + "azure.cosmos", + "opentelemetry.sdk", + "azure.monitor.opentelemetry.exporter.export._base", +) + class Application_Base(ABC): application_context: AppContext = None @@ -78,6 +91,17 @@ def __init__(self, env_file_path: str | None = None, **data): ): logging.getLogger(logger_name).setLevel(azure_level) + # Hard-suppress known noisy packages regardless of operator + # config. Without this, the App Insights logs view is + # dominated by per-request HTTP policy logs and per-call + # Cosmos diagnostics — see AC #3 / AC #4 of AB#37816. + # We never lower a logger that the operator has explicitly + # raised below WARNING. + for noisy_pkg in _NOISY_LOGGER_PACKAGES: + noisy_logger = logging.getLogger(noisy_pkg) + if noisy_logger.level == logging.NOTSET or noisy_logger.level < logging.WARNING: + noisy_logger.setLevel(logging.WARNING) + # Initialize the application self.initialize() From 54f756d8ce1e4e539913456e8fd81fe810eeafff Mon Sep 17 00:00:00 2001 From: Shreyas-Microsoft Date: Wed, 6 May 2026 19:20:43 +0530 Subject: [PATCH 03/50] feat(backend-api): emit structured success/error events on routers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Application Insights custom-event emission and per-request span annotation to every business handler in: - routers/router_process.py - routers/router_files.py - routers/router_debug.py For each handler: - On success, emits a `Success` event via `track_event_if_configured` with the relevant domain ids (process_id, file_id, filename, counts, kill_state, ...). - On exception, emits a `Error` event carrying `error`, `error_type`, and (when available) `status_code`. Then calls `current_span.record_exception(e)` and `set_status(StatusCode.ERROR)` so the failure is also visible in the App Insights end-to-end transaction view. - Stamps domain ids onto the active span via `set_attribute` so custom queries on `requests | extend customDimensions` can pivot by `process_id` / `file_id` directly without re-parsing message strings. Two small private helpers `_annotate_span` and `_record_exception_on_span` are duplicated into both `router_process.py` and `router_files.py` (rather than placed in `libs/logging/`) to keep the OpenTelemetry import surface visible at the call site and to avoid a new public helper that the rest of the codebase might accidentally depend on. `routers/http_probes.py` is left untouched — its routes match the `excluded_urls="health,startup"` configured on `FastAPIInstrumentor` in the previous commit and so are intentionally not telemetered. The pre-existing `ILoggerService.log_info(f"...")` / `log_error(...)` calls are preserved as-is. The new code emitted by this commit uses `%s` lazy formatting in its log strings. Validation - `uv run pytest src/tests --no-cov -c /dev/null` -> 44 passed (with `PYTHONPATH=src/app`). - `Application()` smoke-init -> 26 routes registered, all routers import cleanly. Implements AC #3 of AB#37816 (telemetry visibility) and contributes to AC #4 (continuous log collection). Work item: https://dev.azure.com/CSACTOSOL/CSA%20Solutioning/_workitems/edit/37816 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/app/routers/router_debug.py | 56 ++- .../src/app/routers/router_files.py | 70 +++- .../src/app/routers/router_process.py | 374 +++++++++++++++++- 3 files changed, 470 insertions(+), 30 deletions(-) diff --git a/src/backend-api/src/app/routers/router_debug.py b/src/backend-api/src/app/routers/router_debug.py index 1622a5df..aa407ca2 100644 --- a/src/backend-api/src/app/routers/router_debug.py +++ b/src/backend-api/src/app/routers/router_debug.py @@ -1,6 +1,9 @@ from fastapi import APIRouter, Request from fastapi.responses import JSONResponse from libs.base.typed_fastapi import TypedFastAPI +from libs.logging.event_utils import track_event_if_configured +from opentelemetry import trace +from opentelemetry.trace import Status, StatusCode router = APIRouter( prefix="/debug", @@ -15,21 +18,40 @@ async def get_config_debug(request: Request): app: TypedFastAPI = request.app config = app.app_context.configuration - # Return configuration values for debugging - config_dict = { - "app_logging_enable": config.app_logging_enable, - "app_logging_level": config.app_logging_level, - "azure_package_logging_level": config.azure_package_logging_level, - "azure_logging_packages": config.azure_logging_packages, - "cosmos_db_account_url": config.cosmos_db_account_url, - "cosmos_db_database_name": config.cosmos_db_database_name, - "cosmos_db_process_container": config.cosmos_db_process_container, - "cosmos_db_process_log_container": config.cosmos_db_process_log_container, - "storage_account_name": config.storage_account_name, - "storage_account_blob_url": config.storage_account_blob_url, - "storage_account_queue_url": config.storage_account_queue_url, - "storage_account_process_container": config.storage_account_process_container, - "storage_account_process_queue": config.storage_account_process_queue, - } + try: + # Return configuration values for debugging + config_dict = { + "app_logging_enable": config.app_logging_enable, + "app_logging_level": config.app_logging_level, + "azure_package_logging_level": config.azure_package_logging_level, + "azure_logging_packages": config.azure_logging_packages, + "cosmos_db_account_url": config.cosmos_db_account_url, + "cosmos_db_database_name": config.cosmos_db_database_name, + "cosmos_db_process_container": config.cosmos_db_process_container, + "cosmos_db_process_log_container": config.cosmos_db_process_log_container, + "storage_account_name": config.storage_account_name, + "storage_account_blob_url": config.storage_account_blob_url, + "storage_account_queue_url": config.storage_account_queue_url, + "storage_account_process_container": config.storage_account_process_container, + "storage_account_process_queue": config.storage_account_process_queue, + } - return JSONResponse(content={"configuration": config_dict}) + track_event_if_configured("GetConfigDebugSuccess", {}) + return JSONResponse(content={"configuration": config_dict}) + except Exception as e: + track_event_if_configured( + "GetConfigDebugError", + {"error": str(e), "error_type": type(e).__name__}, + ) + # Mirror the exception-recording pattern from the other routers + # so the debug endpoint participates in the same App Insights view. + span = trace.get_current_span() + if span is not None and span.is_recording(): + try: + span.record_exception(e) + span.set_status( + Status(StatusCode.ERROR, description=type(e).__name__) + ) + except Exception: # noqa: BLE001 + pass + raise diff --git a/src/backend-api/src/app/routers/router_files.py b/src/backend-api/src/app/routers/router_files.py index aa778f71..1eca87e7 100644 --- a/src/backend-api/src/app/routers/router_files.py +++ b/src/backend-api/src/app/routers/router_files.py @@ -13,14 +13,44 @@ ) from fastapi.responses import Response from libs.base.typed_fastapi import TypedFastAPI +from libs.logging.event_utils import track_event_if_configured from libs.models.entities import File +from libs.repositories.file_repository import FileRepository +from libs.repositories.process_repository import ProcessRepository from libs.sas.storage import AsyncStorageBlobHelper from libs.services.auth import get_authenticated_user from libs.services.input_validation import is_valid_uuid from libs.services.interfaces import ILoggerService +from opentelemetry import trace +from opentelemetry.trace import Status, StatusCode from routers.models.files import FileUploadResult -from libs.repositories.process_repository import ProcessRepository -from libs.repositories.file_repository import FileRepository + + +def _annotate_span(attributes: dict[str, object]) -> None: + """Stamp domain attributes onto the active span, if any.""" + span = trace.get_current_span() + if span is None or not span.is_recording(): + return + for key, value in attributes.items(): + if value is None: + continue + try: + span.set_attribute(key, value) + except Exception: # noqa: BLE001 + pass + + +def _record_exception_on_span(exc: Exception) -> None: + """Record an exception + ERROR status on the active span, if any.""" + span = trace.get_current_span() + if span is None or not span.is_recording(): + return + try: + span.record_exception(exc) + span.set_status(Status(StatusCode.ERROR, description=type(exc).__name__)) + except Exception: # noqa: BLE001 + pass + router = APIRouter( prefix="/api/file", @@ -110,6 +140,23 @@ async def upload_file( f"Process {process_id} source count updated to {process_record.source_file_count}." ) + _annotate_span( + { + "process_id": process_id, + "file_id": file_id, + "filename": file_name, + } + ) + track_event_if_configured( + "UploadFileSuccess", + { + "process_id": process_id, + "file_id": file_id, + "filename": file_name, + "source_file_count": process_record.source_file_count, + }, + ) + return FileUploadResult( batch_id=process_record.id, file_id=file_record.id, @@ -118,7 +165,26 @@ async def upload_file( except HTTPException as e: logger.log_error(f"HTTPException: {e.detail}", e) + track_event_if_configured( + "UploadFileError", + { + "process_id": process_id, + "error": str(e.detail), + "error_type": type(e).__name__, + "status_code": e.status_code, + }, + ) + _record_exception_on_span(e) raise e except Exception as e: logger.log_error(f"Exception: {str(e)}", e) + track_event_if_configured( + "UploadFileError", + { + "process_id": process_id, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) raise HTTPException(status_code=500, detail="Internal server error") from e diff --git a/src/backend-api/src/app/routers/router_process.py b/src/backend-api/src/app/routers/router_process.py index 9851a261..5d8e5552 100644 --- a/src/backend-api/src/app/routers/router_process.py +++ b/src/backend-api/src/app/routers/router_process.py @@ -8,11 +8,14 @@ from fastapi import APIRouter, File, Form, HTTPException, Request, Response, UploadFile from fastapi.responses import JSONResponse, StreamingResponse from libs.base.typed_fastapi import TypedFastAPI +from libs.logging.event_utils import track_event_if_configured from libs.models.entities import Process from libs.repositories.process_repository import ProcessRepository from libs.services.auth import get_authenticated_user from libs.services.interfaces import ILoggerService from libs.services.process_services import ProcessService +from opentelemetry import trace +from opentelemetry.trace import Status, StatusCode from routers.models.files import FileInfo from routers.models.processes import ( FileContentResponse, @@ -26,6 +29,37 @@ FileInfo as ResponseFileInfo, ) + +def _annotate_span(attributes: dict[str, object]) -> None: + """Stamp domain attributes onto the active span, if any. + + No-op when no span is active (e.g. called from a unit test that did + not stand up the OpenTelemetry SDK). + """ + span = trace.get_current_span() + if span is None or not span.is_recording(): + return + for key, value in attributes.items(): + if value is None: + continue + try: + span.set_attribute(key, value) + except Exception: # noqa: BLE001 — telemetry must never break a request + pass + + +def _record_exception_on_span(exc: Exception) -> None: + """Record an exception + ERROR status on the active span, if any.""" + span = trace.get_current_span() + if span is None or not span.is_recording(): + return + try: + span.record_exception(exc) + span.set_status(Status(StatusCode.ERROR, description=type(exc).__name__)) + except Exception: # noqa: BLE001 + pass + + router = APIRouter( prefix="/api/process", tags=["process"], @@ -64,12 +98,30 @@ async def create(request: Request): processRepository = scope.get_service(ProcessRepository) await processRepository.add_async(process) + _annotate_span({"process_id": process.id}) + track_event_if_configured( + "CreateProcessSuccess", {"process_id": process.id} + ) return ProcessCreateResponse(process_id=process.id) except HTTPException as e: logger.log_error(f"HTTPException: {e.detail}", e) + track_event_if_configured( + "CreateProcessError", + { + "error": str(e.detail), + "error_type": type(e).__name__, + "status_code": e.status_code, + }, + ) + _record_exception_on_span(e) raise e except Exception as e: logger.log_error(f"Exception: {str(e)}", e) + track_event_if_configured( + "CreateProcessError", + {"error": str(e), "error_type": type(e).__name__}, + ) + _record_exception_on_span(e) raise HTTPException(status_code=500, detail="Internal server error") from e @@ -82,11 +134,28 @@ async def status(process_id: str, request: Request): logger_service.log_info( f"Process router status endpoint called for process_id: {process_id}" ) + _annotate_span({"process_id": process_id}) # loading business component for process processService = app.app_context.get_service(ProcessService) - return await processService.get_current_process(process_id) + try: + result = await processService.get_current_process(process_id) + track_event_if_configured( + "GetProcessStatusSuccess", {"process_id": process_id} + ) + return result + except Exception as e: + track_event_if_configured( + "GetProcessStatusError", + { + "process_id": process_id, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) + raise @router.get("/status/{process_id}/render/", response_class=JSONResponse) @@ -98,11 +167,28 @@ async def render_status(process_id: str, request: Request): logger_service.log_info( f"Process router render status endpoint called for process_id: {process_id}" ) + _annotate_span({"process_id": process_id}) # loading business component for process processService = app.app_context.get_service(ProcessService) - return await processService.render_current_process(process_id) + try: + result = await processService.render_current_process(process_id) + track_event_if_configured( + "RenderProcessStatusSuccess", {"process_id": process_id} + ) + return result + except Exception as e: + track_event_if_configured( + "RenderProcessStatusError", + { + "process_id": process_id, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) + raise @router.post(process_router_paths.UPLOAD_FILES, status_code=200) @@ -194,9 +280,33 @@ async def upload_files( if response: response.headers["Location"] = f"/process/{process_id}/" + _annotate_span( + { + "process_id": process_id, + "uploaded_count": len(uploaded_files), + "total_count": len(all_process_files), + } + ) + track_event_if_configured( + "UploadFilesSuccess", + { + "process_id": process_id, + "uploaded_count": len(uploaded_files), + "total_count": len(all_process_files), + }, + ) return result_response except Exception as e: logger_service.log_error(f"Error in upload_files: {str(e)}") + track_event_if_configured( + "UploadFilesError", + { + "process_id": process_id, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) raise HTTPException(status_code=500, detail=f"Error uploading files: {str(e)}") @@ -267,15 +377,49 @@ async def delete_file( if response: response.headers["Location"] = f"/process/{process_id}/" + _annotate_span( + { + "process_id": process_id, + "deleted_file": file_name, + "remaining_count": len(all_process_files), + } + ) + track_event_if_configured( + "DeleteFileSuccess", + { + "process_id": process_id, + "deleted_file": file_name, + "remaining_count": len(all_process_files), + }, + ) return result_response - except FileNotFoundError: + except FileNotFoundError as e: + track_event_if_configured( + "DeleteFileError", + { + "process_id": process_id, + "deleted_file": file_name, + "error_type": "FileNotFoundError", + }, + ) + _record_exception_on_span(e) raise HTTPException( status_code=404, detail=f"File '{file_name}' not found for process '{process_id}'", ) except Exception as e: logger_service.log_error(f"Error in delete_file: {str(e)}") + track_event_if_configured( + "DeleteFileError", + { + "process_id": process_id, + "deleted_file": file_name, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) raise HTTPException(status_code=500, detail=f"Error deleting file: {str(e)}") @@ -331,10 +475,26 @@ async def delete_process( if response: response.headers["Location"] = f"/process/{process_id}/" + _annotate_span( + {"process_id": process_id, "deleted_count": deleted_count} + ) + track_event_if_configured( + "DeleteProcessSuccess", + {"process_id": process_id, "deleted_count": deleted_count}, + ) return result_response except Exception as e: logger_service.log_error(f"Error in delete_process: {str(e)}") + track_event_if_configured( + "DeleteProcessError", + { + "process_id": process_id, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) raise HTTPException(status_code=500, detail=f"Error deleting process: {str(e)}") @@ -388,6 +548,10 @@ async def start_processing( if response: response.headers["Location"] = f"/process/{process_id}/" + _annotate_span({"process_id": process_id}) + track_event_if_configured( + "StartProcessingSuccess", {"process_id": process_id} + ) return { "message": "Processing started successfully", "process_id": process_id, @@ -396,6 +560,15 @@ async def start_processing( } except Exception as e: logger_service.log_error(f"Error in start_processing: {str(e)}") + track_event_if_configured( + "StartProcessingError", + { + "process_id": process_id, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) raise HTTPException( status_code=500, detail=f"Error starting processing: {str(e)}" ) @@ -449,6 +622,17 @@ async def download_process_files( f"Created ZIP file with {len(converted_files)} files for process {process_id}" ) + _annotate_span( + { + "process_id": process_id, + "file_count": len(converted_files), + } + ) + track_event_if_configured( + "DownloadProcessFilesSuccess", + {"process_id": process_id, "file_count": len(converted_files)}, + ) + # Return ZIP file as streaming response return StreamingResponse( io.BytesIO(zip_buffer.read()), @@ -460,9 +644,28 @@ async def download_process_files( except HTTPException as e: logger_service.log_error(f"HTTPException in download: {e.detail}") + track_event_if_configured( + "DownloadProcessFilesError", + { + "process_id": process_id, + "error": str(e.detail), + "error_type": type(e).__name__, + "status_code": e.status_code, + }, + ) + _record_exception_on_span(e) raise e except Exception as e: logger_service.log_error(f"Error in download_process_files: {str(e)}") + track_event_if_configured( + "DownloadProcessFilesError", + { + "process_id": process_id, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) raise HTTPException( status_code=500, detail=f"Error downloading files: {str(e)}" ) @@ -512,13 +715,40 @@ async def get_process_summary( f"Process summary retrieved for {process_id}: {len(filenames)} files" ) + _annotate_span( + {"process_id": process_id, "file_count": len(filenames)} + ) + track_event_if_configured( + "GetProcessSummarySuccess", + {"process_id": process_id, "file_count": len(filenames)}, + ) + return response except HTTPException as e: logger_service.log_error(f"HTTPException in process summary: {e.detail}") + track_event_if_configured( + "GetProcessSummaryError", + { + "process_id": process_id, + "error": str(e.detail), + "error_type": type(e).__name__, + "status_code": e.status_code, + }, + ) + _record_exception_on_span(e) raise e except Exception as e: logger_service.log_error(f"Error in get_process_summary: {str(e)}") + track_event_if_configured( + "GetProcessSummaryError", + { + "process_id": process_id, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) raise HTTPException( status_code=500, detail=f"Error retrieving process summary: {str(e)}" ) @@ -561,23 +791,68 @@ async def get_file_content( f"File content retrieved for {filename} in process {process_id}" ) + _annotate_span({"process_id": process_id, "filename": filename}) + track_event_if_configured( + "GetFileContentSuccess", + {"process_id": process_id, "filename": filename}, + ) + return FileContentResponse(content=file_content) - except FileNotFoundError: + except FileNotFoundError as e: + track_event_if_configured( + "GetFileContentError", + { + "process_id": process_id, + "filename": filename, + "error_type": "FileNotFoundError", + }, + ) + _record_exception_on_span(e) raise HTTPException( status_code=404, detail=f"File '{filename}' not found for process '{process_id}'", ) - except UnicodeDecodeError: + except UnicodeDecodeError as e: + track_event_if_configured( + "GetFileContentError", + { + "process_id": process_id, + "filename": filename, + "error_type": "UnicodeDecodeError", + }, + ) + _record_exception_on_span(e) raise HTTPException( status_code=400, detail=f"File '{filename}' is not a text file and cannot be displayed", ) except HTTPException as e: logger_service.log_error(f"HTTPException in file content: {e.detail}") + track_event_if_configured( + "GetFileContentError", + { + "process_id": process_id, + "filename": filename, + "error": str(e.detail), + "error_type": type(e).__name__, + "status_code": e.status_code, + }, + ) + _record_exception_on_span(e) raise e except Exception as e: logger_service.log_error(f"Error in get_file_content: {str(e)}") + track_event_if_configured( + "GetFileContentError", + { + "process_id": process_id, + "filename": filename, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) raise HTTPException( status_code=500, detail=f"Error retrieving file content: {str(e)}" ) @@ -599,6 +874,7 @@ async def cancel_process( try: logger_service.log_info(f"Cancel process request for process_id: {process_id}") + _annotate_span({"process_id": process_id}) # Get authenticated user authenticated_user = get_authenticated_user(request) @@ -653,6 +929,14 @@ async def cancel_process( f"Cancel request sent for process {process_id}, state: {result.get('kill_state', 'unknown')}" ) + track_event_if_configured( + "CancelProcessSuccess", + { + "process_id": process_id, + "kill_state": result.get("kill_state", "pending"), + }, + ) + return { "message": "Cancellation request submitted", "process_id": process_id, @@ -661,22 +945,57 @@ async def cancel_process( "kill_requested_at": result.get("kill_requested_at", ""), } - except httpx.TimeoutException: + except httpx.TimeoutException as e: logger_service.log_error(f"Timeout connecting to processor control API") + track_event_if_configured( + "CancelProcessError", + { + "process_id": process_id, + "error_type": "TimeoutException", + }, + ) + _record_exception_on_span(e) raise HTTPException( status_code=504, detail="Timeout connecting to processor control API", ) - except httpx.ConnectError: + except httpx.ConnectError as e: logger_service.log_error(f"Failed to connect to processor control API") + track_event_if_configured( + "CancelProcessError", + { + "process_id": process_id, + "error_type": "ConnectError", + }, + ) + _record_exception_on_span(e) raise HTTPException( status_code=503, detail="Processor control API is unavailable", ) - except HTTPException: + except HTTPException as e: + track_event_if_configured( + "CancelProcessError", + { + "process_id": process_id, + "error": str(e.detail), + "error_type": type(e).__name__, + "status_code": e.status_code, + }, + ) + _record_exception_on_span(e) raise except Exception as e: logger_service.log_error(f"Error in cancel_process: {str(e)}") + track_event_if_configured( + "CancelProcessError", + { + "process_id": process_id, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) raise HTTPException( status_code=500, detail=f"Error cancelling process: {str(e)}" ) @@ -696,6 +1015,7 @@ async def get_cancel_status( try: logger_service.log_info(f"Get cancel status for process_id: {process_id}") + _annotate_span({"process_id": process_id}) # Get authenticated user authenticated_user = get_authenticated_user(request) @@ -745,24 +1065,56 @@ async def get_cancel_status( result = response.json() + track_event_if_configured( + "GetCancelStatusSuccess", {"process_id": process_id} + ) return result - except httpx.TimeoutException: + except httpx.TimeoutException as e: logger_service.log_error(f"Timeout connecting to processor control API") + track_event_if_configured( + "GetCancelStatusError", + {"process_id": process_id, "error_type": "TimeoutException"}, + ) + _record_exception_on_span(e) raise HTTPException( status_code=504, detail="Timeout connecting to processor control API", ) - except httpx.ConnectError: + except httpx.ConnectError as e: logger_service.log_error(f"Failed to connect to processor control API") + track_event_if_configured( + "GetCancelStatusError", + {"process_id": process_id, "error_type": "ConnectError"}, + ) + _record_exception_on_span(e) raise HTTPException( status_code=503, detail="Processor control API is unavailable", ) - except HTTPException: + except HTTPException as e: + track_event_if_configured( + "GetCancelStatusError", + { + "process_id": process_id, + "error": str(e.detail), + "error_type": type(e).__name__, + "status_code": e.status_code, + }, + ) + _record_exception_on_span(e) raise except Exception as e: logger_service.log_error(f"Error in get_cancel_status: {str(e)}") + track_event_if_configured( + "GetCancelStatusError", + { + "process_id": process_id, + "error": str(e), + "error_type": type(e).__name__, + }, + ) + _record_exception_on_span(e) raise HTTPException( status_code=500, detail=f"Error getting cancel status: {str(e)}" ) From 992376874f89bd15a4d68e604b2e63d49f26f6c1 Mon Sep 17 00:00:00 2001 From: Shreyas-Microsoft Date: Wed, 6 May 2026 19:23:17 +0530 Subject: [PATCH 04/50] test(backend-api): cover track_event_if_configured Application Insights gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the first dedicated test package for the new `libs/logging/event_utils.py` helper at `src/tests/logging/test_event_utils.py` (mirroring the existing `src/tests//test_.py` layout — no new tooling, no new config). Coverage: - `test_no_op_when_connection_string_unset`: with the env var absent, the helper does NOT call `track_event` and the SDK module is never imported (verified via a `sys.modules` fake). - `test_warning_fires_only_once_per_process`: subsequent unconfigured calls are silent (one-shot warning latch). - `test_unconfigured_warning_message_is_stable`: pins the exact warning template that the rest of the system asserts against. - `test_forwards_to_track_event_when_configured`: with the env var set, the helper forwards the (name, properties) pair through to `azure.monitor.events.extension.track_event`. - `test_none_properties_normalised_to_empty_dict`: `properties=None` surfaces as `{}` to the SDK so `customDimensions` is always an object. - `test_whitespace_only_connection_string_is_treated_as_unset`: belt-and-braces gating against accidentally-blank env values. - `test_sdk_exception_is_swallowed`: a `RuntimeError` thrown by the SDK is caught and logged; telemetry must never break a request. The tests use a `sys.modules` fake for `azure.monitor.events.extension` so they never import the real Azure Monitor SDK at runtime — keeps the suite hermetic, fast (<1s), and CI-friendly. Validation - `uv run pytest src/tests --no-cov -c /dev/null` -> 51 passed (44 pre-existing + 7 new), with `PYTHONPATH=src/app`. Implements verification scope of AC #3 of AB#37816. Work item: https://dev.azure.com/CSACTOSOL/CSA%20Solutioning/_workitems/edit/37816 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/backend-api/src/tests/logging/__init__.py | 0 .../src/tests/logging/test_event_utils.py | 190 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 src/backend-api/src/tests/logging/__init__.py create mode 100644 src/backend-api/src/tests/logging/test_event_utils.py diff --git a/src/backend-api/src/tests/logging/__init__.py b/src/backend-api/src/tests/logging/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend-api/src/tests/logging/test_event_utils.py b/src/backend-api/src/tests/logging/test_event_utils.py new file mode 100644 index 00000000..c05b840c --- /dev/null +++ b/src/backend-api/src/tests/logging/test_event_utils.py @@ -0,0 +1,190 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Unit tests for ``libs.logging.event_utils.track_event_if_configured``. + +These tests exercise the gating behaviour that the rest of the +application depends on: + +* When ``APPLICATIONINSIGHTS_CONNECTION_STRING`` is unset / empty, the + call is a no-op and the underlying SDK is *never* imported. +* When the env var is set, the call forwards through to + ``azure.monitor.events.extension.track_event``. +* The "missing connection string" warning fires exactly once per + process, with the wording the rest of the system asserts against. +* SDK exceptions are swallowed so telemetry can never break a request. + +The tests deliberately avoid touching the real Azure Monitor SDK by +patching the symbol that the implementation imports lazily inside the +function. This keeps the suite hermetic and CI-friendly. +""" +from __future__ import annotations + +import logging +import sys +import types +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from libs.logging import event_utils +from libs.logging.event_utils import ( + APP_INSIGHTS_CONN_STRING_ENV, + reset_unconfigured_warning_for_tests, + track_event_if_configured, +) + + +@pytest.fixture(autouse=True) +def _reset_module_state(monkeypatch: pytest.MonkeyPatch) -> None: + """Each test starts from a clean slate. + + Removes any process-wide env that would skew gating, resets the + one-shot warning latch, and removes any cached + ``azure.monitor.events.extension`` module so the lazy import path + inside ``track_event_if_configured`` is exercised fresh on each + test. + """ + monkeypatch.delenv(APP_INSIGHTS_CONN_STRING_ENV, raising=False) + reset_unconfigured_warning_for_tests() + sys.modules.pop("azure.monitor.events.extension", None) + + +def _install_fake_track_event(call_log: list[tuple[str, dict[str, Any]]]) -> MagicMock: + """Inject a fake ``azure.monitor.events.extension`` into ``sys.modules``. + + The implementation in ``event_utils`` does + ``from azure.monitor.events.extension import track_event`` lazily; + seeding ``sys.modules`` with a fake ensures we never hit the real + SDK during tests and keeps the assertion surface narrow. + """ + mock_track_event = MagicMock( + side_effect=lambda name, properties: call_log.append((name, properties)) + ) + fake_module = types.ModuleType("azure.monitor.events.extension") + fake_module.track_event = mock_track_event # type: ignore[attr-defined] + sys.modules["azure.monitor.events.extension"] = fake_module + return mock_track_event + + +def test_no_op_when_connection_string_unset( + caplog: pytest.LogCaptureFixture, +) -> None: + """With no env var, the helper must not call into the SDK at all.""" + call_log: list[tuple[str, dict[str, Any]]] = [] + mock_track_event = _install_fake_track_event(call_log) + + with caplog.at_level(logging.WARNING, logger=event_utils.logger.name): + track_event_if_configured("CreateProcessSuccess", {"process_id": "abc"}) + + mock_track_event.assert_not_called() + assert call_log == [] + # Warning fires exactly once and references the event name. + warnings = [r for r in caplog.records if r.levelno == logging.WARNING] + assert len(warnings) == 1 + assert "APPLICATIONINSIGHTS_CONNECTION_STRING is not set" in warnings[0].getMessage() + assert "CreateProcessSuccess" in warnings[0].getMessage() + + +def test_warning_fires_only_once_per_process( + caplog: pytest.LogCaptureFixture, +) -> None: + """Subsequent unconfigured calls must be silent (one-shot warning latch).""" + _install_fake_track_event([]) + + with caplog.at_level(logging.WARNING, logger=event_utils.logger.name): + track_event_if_configured("First", {"k": 1}) + track_event_if_configured("Second", {"k": 2}) + track_event_if_configured("Third", {"k": 3}) + + warnings = [r for r in caplog.records if r.levelno == logging.WARNING] + assert len(warnings) == 1, "Only the first unconfigured call should warn." + + +def test_unconfigured_warning_message_is_stable() -> None: + """The exact warning template is part of the helper's contract.""" + assert event_utils._UNCONFIGURED_WARNING == ( + "APPLICATIONINSIGHTS_CONNECTION_STRING is not set; " + "track_event_if_configured(name=%s) is a no-op." + ) + + +def test_forwards_to_track_event_when_configured( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A non-empty connection string must route the call to the SDK.""" + monkeypatch.setenv( + APP_INSIGHTS_CONN_STRING_ENV, + "InstrumentationKey=00000000-0000-0000-0000-000000000000", + ) + call_log: list[tuple[str, dict[str, Any]]] = [] + mock_track_event = _install_fake_track_event(call_log) + + track_event_if_configured( + "UploadFilesSuccess", + {"process_id": "p-1", "uploaded_count": 3}, + ) + + mock_track_event.assert_called_once_with( + "UploadFilesSuccess", + {"process_id": "p-1", "uploaded_count": 3}, + ) + assert call_log == [ + ("UploadFilesSuccess", {"process_id": "p-1", "uploaded_count": 3}) + ] + + +def test_none_properties_normalised_to_empty_dict( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Passing ``properties=None`` must surface as an empty dict to the SDK.""" + monkeypatch.setenv( + APP_INSIGHTS_CONN_STRING_ENV, + "InstrumentationKey=00000000-0000-0000-0000-000000000000", + ) + call_log: list[tuple[str, dict[str, Any]]] = [] + _install_fake_track_event(call_log) + + track_event_if_configured("StartProcessingSuccess") + + assert call_log == [("StartProcessingSuccess", {})] + + +def test_whitespace_only_connection_string_is_treated_as_unset( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A whitespace-only env var must not be considered configured.""" + monkeypatch.setenv(APP_INSIGHTS_CONN_STRING_ENV, " ") + call_log: list[tuple[str, dict[str, Any]]] = [] + mock_track_event = _install_fake_track_event(call_log) + + track_event_if_configured("Anything") + + mock_track_event.assert_not_called() + assert call_log == [] + + +def test_sdk_exception_is_swallowed( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """An exception from ``track_event`` must be caught and logged, not raised.""" + monkeypatch.setenv( + APP_INSIGHTS_CONN_STRING_ENV, + "InstrumentationKey=00000000-0000-0000-0000-000000000000", + ) + fake_module = types.ModuleType("azure.monitor.events.extension") + fake_module.track_event = MagicMock( # type: ignore[attr-defined] + side_effect=RuntimeError("exporter dead") + ) + sys.modules["azure.monitor.events.extension"] = fake_module + + with caplog.at_level(logging.ERROR, logger=event_utils.logger.name): + # Must not raise even though the underlying SDK does. + track_event_if_configured("DeleteProcessSuccess", {"process_id": "p-9"}) + + errors = [r for r in caplog.records if r.levelno == logging.ERROR] + assert any( + "Failed to publish App Insights custom event" in r.getMessage() + for r in errors + ) From 70fe7cbed6e1dfb3e2066a931e59012588d8f65b Mon Sep 17 00:00:00 2001 From: Shreyas-Microsoft Date: Wed, 6 May 2026 19:25:44 +0530 Subject: [PATCH 05/50] fix(infra): remove redundant diagnosticSettings on Application Insights AVM module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AVM `insights/component@0.6.0` module already wires Application Insights to Log Analytics through the `workspaceResourceId` parameter (workspace-based App Insights). Adding a separate `diagnosticSettings` entry pointing at the SAME workspace causes the platform-level `AppAvailabilityResults`, `AppRequests`, etc. tables to be ingested twice — once via the workspace-based App Insights pipeline and again via the diagnostic-settings forwarder. Removes the redundant line from both: - `infra/main.bicep` (line 305) - `infra/main_custom.bicep` (line 283) The custom-event / OpenTelemetry path the rest of this branch wires up sends data straight to App Insights via `APPLICATIONINSIGHTS_CONNECTION_STRING`, which is already injected into the backend-api and processor container apps by the same Bicep files (no change required to those env-var blocks). Validation - `bicep build infra/main.bicep --outfile ` -> OK, no diagnostics. - `bicep build infra/main_custom.bicep --outfile ` -> OK, only a pre-existing BCP334 warning at line 694 (unrelated to this edit). Implements AC #4 of AB#37816 (continuous, non-duplicated log collection). Reference: https://github.com/microsoft/Conversation-Knowledge-Mining-Solution-Accelerator/pull/811 Work item: https://dev.azure.com/CSACTOSOL/CSA%20Solutioning/_workitems/edit/37816 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- infra/main.bicep | 6 +++++- infra/main_custom.bicep | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index d229d716..904fee03 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -301,8 +301,12 @@ module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (en disableIpMasking: false flowType: 'Bluefield' // WAF aligned configuration for Monitoring + // The AVM `insights/component` module wires Application Insights to + // the Log Analytics workspace via `workspaceResourceId` (workspace- + // based App Insights). A separate `diagnosticSettings` entry on the + // SAME workspace causes duplicate ingestion of platform logs. + // Source: AB#37816 — see CKM #811 reference implementation. workspaceResourceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : '' - diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null } } diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index fea69ac3..f93b93ab 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -279,8 +279,12 @@ module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (en disableIpMasking: false flowType: 'Bluefield' // WAF aligned configuration for Monitoring + // The AVM `insights/component` module wires Application Insights to + // the Log Analytics workspace via `workspaceResourceId` (workspace- + // based App Insights). A separate `diagnosticSettings` entry on the + // SAME workspace causes duplicate ingestion of platform logs. + // Source: AB#37816 — see CKM #811 reference implementation. workspaceResourceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : '' - diagnosticSettings: enableMonitoring ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }] : null } } From 6589e611ebd075a661cba3d44ad194d11119e577 Mon Sep 17 00:00:00 2001 From: Shreyas-Microsoft Date: Tue, 19 May 2026 15:02:09 +0530 Subject: [PATCH 06/50] fix(backend-api): include error message in CancelProcess/CancelStatus timeout/connect events Aligns the property shape of the Timeout/Connect error events with every other error event emitted by the router (which all include both `error` and `error_type`). Without `error`, an App Insights query like `customEvents | where customDimensions.error contains "..."` silently misses these two failure paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/backend-api/src/app/routers/router_process.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/backend-api/src/app/routers/router_process.py b/src/backend-api/src/app/routers/router_process.py index 5d8e5552..3709a5fe 100644 --- a/src/backend-api/src/app/routers/router_process.py +++ b/src/backend-api/src/app/routers/router_process.py @@ -951,6 +951,7 @@ async def cancel_process( "CancelProcessError", { "process_id": process_id, + "error": str(e), "error_type": "TimeoutException", }, ) @@ -965,6 +966,7 @@ async def cancel_process( "CancelProcessError", { "process_id": process_id, + "error": str(e), "error_type": "ConnectError", }, ) @@ -1074,7 +1076,7 @@ async def get_cancel_status( logger_service.log_error(f"Timeout connecting to processor control API") track_event_if_configured( "GetCancelStatusError", - {"process_id": process_id, "error_type": "TimeoutException"}, + {"process_id": process_id, "error": str(e), "error_type": "TimeoutException"}, ) _record_exception_on_span(e) raise HTTPException( @@ -1085,7 +1087,7 @@ async def get_cancel_status( logger_service.log_error(f"Failed to connect to processor control API") track_event_if_configured( "GetCancelStatusError", - {"process_id": process_id, "error_type": "ConnectError"}, + {"process_id": process_id, "error": str(e), "error_type": "ConnectError"}, ) _record_exception_on_span(e) raise HTTPException( From 1bfdb56a2c68757d8706e29f25824fb6cd8257ab Mon Sep 17 00:00:00 2001 From: Priyanka-Microsoft Date: Mon, 25 May 2026 14:44:56 +0530 Subject: [PATCH 07/50] token usage --- infra/dashboards/deploy-workbooks.ps1 | 86 ++++ infra/dashboards/token-usage-queries.kql | 113 +++++ infra/dashboards/workbook-eks-content.json | 1 + infra/dashboards/workbook-gke-content.json | 1 + infra/main.bicep | 12 +- infra/main.parameters.json | 23 +- infra/main_custom.bicep | 12 +- infra/modules/tokenUsageWorkbook.bicep | 458 ++++++++++++++++++ src/frontend/Dockerfile | 1 + src/processor/Dockerfile | 3 +- src/processor/pyproject.toml | 2 + .../azure_openai_response_retry.py | 181 ++++++- .../agent_framework/groupchat_orchestrator.py | 164 +++++++ .../src/libs/base/orchestrator_base.py | 23 +- src/processor/src/main.py | 29 ++ src/processor/src/main_service.py | 33 ++ src/processor/src/services/queue_service.py | 1 + .../src/steps/analysis/models/step_param.py | 1 + .../orchestration/analysis_orchestrator.py | 1 + .../yaml_convert_orchestrator.py | 1 + .../orchestration/design_orchestrator.py | 1 + .../documentation_orchestrator.py | 1 + .../src/steps/migration_processor.py | 22 + src/processor/src/utils/agent_telemetry.py | 66 +++ src/processor/src/utils/event_utils.py | 64 +++ .../src/utils/token_usage_tracker.py | 403 +++++++++++++++ 26 files changed, 1694 insertions(+), 9 deletions(-) create mode 100644 infra/dashboards/deploy-workbooks.ps1 create mode 100644 infra/dashboards/token-usage-queries.kql create mode 100644 infra/dashboards/workbook-eks-content.json create mode 100644 infra/dashboards/workbook-gke-content.json create mode 100644 infra/modules/tokenUsageWorkbook.bicep create mode 100644 src/processor/src/utils/event_utils.py create mode 100644 src/processor/src/utils/token_usage_tracker.py diff --git a/infra/dashboards/deploy-workbooks.ps1 b/infra/dashboards/deploy-workbooks.ps1 new file mode 100644 index 00000000..caa88198 --- /dev/null +++ b/infra/dashboards/deploy-workbooks.ps1 @@ -0,0 +1,86 @@ +# ============================================================= +# LLM Token Usage Workbook Deployment Script +# ============================================================= +# Usage: +# .\deploy-workbooks.ps1 -ResourceGroup -AppInsightsResourceId [-Location ] +# +# Example: +# .\deploy-workbooks.ps1 ` +# -ResourceGroup "rg-my-permanent-rg" ` +# -AppInsightsResourceId "/subscriptions//resourcegroups//providers/microsoft.insights/components/" ` +# -Location "australiaeast" +# ============================================================= + +param( + [Parameter(Mandatory=$true)] + [string]$ResourceGroup, + + [Parameter(Mandatory=$true)] + [string]$AppInsightsResourceId, + + [string]$Location = "australiaeast" +) + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +# Deploy GKE workbook +$gkeContent = Get-Content "$scriptDir\workbook-gke-content.json" -Raw +$gkeId = [guid]::NewGuid().ToString() + +$body = @{ + location = $Location + kind = "shared" + properties = @{ + displayName = "LLM Token Usage Dashboard - GKE" + serializedData = $gkeContent + version = "Notebook/1.0" + sourceId = $AppInsightsResourceId + category = "workbook" + } + tags = @{ + "hidden-title" = "LLM Token Usage Dashboard - GKE" + } +} | ConvertTo-Json -Depth 5 + +$bodyFile = [System.IO.Path]::GetTempFileName() +$body | Set-Content $bodyFile -Encoding UTF8 + +az rest --method PUT ` + --url "https://management.azure.com/subscriptions/$(az account show --query id -o tsv)/resourceGroups/$ResourceGroup/providers/microsoft.insights/workbooks/$($gkeId)?api-version=2022-04-01" ` + --body "@$bodyFile" ` + --headers "Content-Type=application/json" 2>&1 | Out-Null + +Write-Host "Deployed GKE workbook: $gkeId" +Remove-Item $bodyFile + +# Deploy EKS workbook +$eksContent = Get-Content "$scriptDir\workbook-eks-content.json" -Raw +$eksId = [guid]::NewGuid().ToString() + +$body = @{ + location = $Location + kind = "shared" + properties = @{ + displayName = "LLM Token Usage Dashboard - EKS" + serializedData = $eksContent + version = "Notebook/1.0" + sourceId = $AppInsightsResourceId + category = "workbook" + } + tags = @{ + "hidden-title" = "LLM Token Usage Dashboard - EKS" + } +} | ConvertTo-Json -Depth 5 + +$bodyFile = [System.IO.Path]::GetTempFileName() +$body | Set-Content $bodyFile -Encoding UTF8 + +az rest --method PUT ` + --url "https://management.azure.com/subscriptions/$(az account show --query id -o tsv)/resourceGroups/$ResourceGroup/providers/microsoft.insights/workbooks/$($eksId)?api-version=2022-04-01" ` + --body "@$bodyFile" ` + --headers "Content-Type=application/json" 2>&1 | Out-Null + +Write-Host "Deployed EKS workbook: $eksId" +Remove-Item $bodyFile + +Write-Host "`nDone! Both workbooks deployed to $ResourceGroup" diff --git a/infra/dashboards/token-usage-queries.kql b/infra/dashboards/token-usage-queries.kql new file mode 100644 index 00000000..38a0fc19 --- /dev/null +++ b/infra/dashboards/token-usage-queries.kql @@ -0,0 +1,113 @@ +// ============================================================================= +// LLM Token Usage Dashboard Queries for Application Insights +// ============================================================================= +// These KQL queries can be used in Azure Application Insights / Log Analytics +// to visualize token usage across agents, models, steps, and users. +// ============================================================================= + +// ---- 1. Overall Token Usage Summary (last 24h) ---- +customEvents +| where name == "LLM_Token_Usage_Summary" +| where timestamp > ago(24h) +| extend process_id = tostring(customDimensions.process_id), + total_input = toint(customDimensions.total_input_tokens), + total_output = toint(customDimensions.total_output_tokens), + total = toint(customDimensions.total_tokens), + call_count = toint(customDimensions.total_calls) +| project timestamp, process_id, total_input, total_output, total, call_count +| order by timestamp desc + +// ---- 2. Per-Agent Token Usage ---- +customEvents +| where name == "LLM_Agent_Token_Usage" +| where timestamp > ago(24h) +| extend agent_name = tostring(customDimensions.agent_name), + input_tokens = toint(customDimensions.input_tokens), + output_tokens = toint(customDimensions.output_tokens), + total_tokens = toint(customDimensions.total_tokens), + calls = toint(customDimensions.call_count), + process_id = tostring(customDimensions.process_id) +| summarize total_input = sum(input_tokens), + total_output = sum(output_tokens), + total = sum(total_tokens), + total_calls = sum(calls) + by agent_name +| order by total desc + +// ---- 3. Per-Model Token Usage ---- +customEvents +| where name == "LLM_Model_Token_Usage" +| where timestamp > ago(24h) +| extend model_name = tostring(customDimensions.model_deployment_name), + input_tokens = toint(customDimensions.input_tokens), + output_tokens = toint(customDimensions.output_tokens), + total_tokens = toint(customDimensions.total_tokens), + calls = toint(customDimensions.call_count), + process_id = tostring(customDimensions.process_id) +| summarize total_input = sum(input_tokens), + total_output = sum(output_tokens), + total = sum(total_tokens), + total_calls = sum(calls) + by model_name +| order by total desc + +// ---- 4. Per-Step (Team) Token Usage ---- +customEvents +| where name == "LLM_Step_Token_Usage" +| where timestamp > ago(24h) +| extend step_name = tostring(customDimensions.step_name), + input_tokens = toint(customDimensions.input_tokens), + output_tokens = toint(customDimensions.output_tokens), + total_tokens = toint(customDimensions.total_tokens), + calls = toint(customDimensions.call_count), + process_id = tostring(customDimensions.process_id) +| summarize total_input = sum(input_tokens), + total_output = sum(output_tokens), + total = sum(total_tokens), + total_calls = sum(calls) + by step_name +| order by total desc + +// ---- 5. Per-User Token Usage (requires user_id in process telemetry) ---- +customEvents +| where name == "LLM_Token_Usage_Summary" +| where timestamp > ago(24h) +| extend process_id = tostring(customDimensions.process_id), + total_tokens = toint(customDimensions.total_tokens), + user_id = tostring(customDimensions.user_id) +| summarize total = sum(total_tokens), runs = count() by user_id +| order by total desc + +// ---- 6. Individual LLM Call Log ---- +customEvents +| where name == "LLM_Token_Usage" +| where timestamp > ago(24h) +| extend agent_name = tostring(customDimensions.agent_name), + step_name = tostring(customDimensions.step_name), + model = tostring(customDimensions.model_deployment_name), + input_tokens = toint(customDimensions.input_tokens), + output_tokens = toint(customDimensions.output_tokens), + total_tokens = toint(customDimensions.total_tokens), + process_id = tostring(customDimensions.process_id) +| project timestamp, process_id, agent_name, step_name, model, input_tokens, output_tokens, total_tokens +| order by timestamp desc + +// ---- 7. Hourly Token Usage Trend ---- +customEvents +| where name == "LLM_Token_Usage" +| where timestamp > ago(7d) +| extend total_tokens = toint(customDimensions.total_tokens) +| summarize hourly_tokens = sum(total_tokens), calls = count() by bin(timestamp, 1h) +| order by timestamp asc +| render timechart + +// ---- 8. Estimated Cost (GPT-4o pricing: $2.50/1M input, $10/1M output) ---- +customEvents +| where name == "LLM_Token_Usage_Summary" +| where timestamp > ago(24h) +| extend process_id = tostring(customDimensions.process_id), + input_tokens = toint(customDimensions.total_input_tokens), + output_tokens = toint(customDimensions.total_output_tokens) +| extend estimated_cost_usd = (input_tokens / 1000000.0 * 2.50) + (output_tokens / 1000000.0 * 10.0) +| project timestamp, process_id, input_tokens, output_tokens, estimated_cost_usd +| order by estimated_cost_usd desc diff --git a/infra/dashboards/workbook-eks-content.json b/infra/dashboards/workbook-eks-content.json new file mode 100644 index 00000000..04433e99 --- /dev/null +++ b/infra/dashboards/workbook-eks-content.json @@ -0,0 +1 @@ +{"version":"Notebook/1.0","items":[{"type":1,"content":{"json":"# LLM Token Usage Dashboard\n\nThis workbook provides comprehensive visibility into LLM token consumption across agents, models, workflow steps, and users.\n\n---"},"name":"header"},{"type":9,"content":{"version":"KqlParameterItem/1.0","parameters":[{"id":"time-range-param","version":"KqlParameterItem/1.0","name":"TimeRange","type":4,"isRequired":true,"value":{"durationMs":1800000,"endTime":"2026-05-21T06:50:00.000Z"},"typeSettings":{"selectableValues":[{"durationMs":3600000},{"durationMs":14400000},{"durationMs":86400000},{"durationMs":259200000},{"durationMs":604800000},{"durationMs":2592000000}],"allowCustom":true},"label":"Time Range"}],"style":"pills","queryType":0,"resourceType":"microsoft.insights/components"},"name":"parameters"},{"type":1,"content":{"json":"## Overall Token Usage Summary"},"name":"summary-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| summarize \n total_input = sum(toint(customDimensions.total_input_tokens)),\n total_output = sum(toint(customDimensions.total_output_tokens)),\n total = sum(toint(customDimensions.total_tokens)),\n total_calls = sum(toint(customDimensions.total_calls)),\n processes = dcount(tostring(customDimensions.process_id))","size":4,"title":"Token Usage Totals","queryType":0,"resourceType":"microsoft.insights/components","visualization":"tiles","tileSettings":{"titleContent":{"columnMatch":"Column1","formatter":1},"leftContent":{"columnMatch":"total","formatter":12,"formatOptions":{"palette":"auto"},"numberFormat":{"unit":0,"options":{"style":"decimal","maximumFractionDigits":0}}},"showBorder":true}},"name":"summary-tiles"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_input = toint(customDimensions.total_input_tokens),\n total_output = toint(customDimensions.total_output_tokens),\n total = toint(customDimensions.total_tokens),\n call_count = toint(customDimensions.total_calls)\n| project timestamp, process_id, total_input, total_output, total, call_count\n| order by timestamp desc","size":0,"title":"Token Usage by Process","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"blue"}}]}},"name":"summary-table"},{"type":1,"content":{"json":"## Per-Agent Token Usage"},"name":"agent-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Agent_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by agent_name\n| order by total desc","size":0,"title":"Token Consumption by Agent","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"blue"}}]}},"customWidth":"50","name":"agent-table"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Agent_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by agent_name\n| order by total desc","size":0,"title":"Token Distribution by Agent","queryType":0,"resourceType":"microsoft.insights/components","visualization":"piechart"},"customWidth":"50","name":"agent-chart"},{"type":1,"content":{"json":"## Per-Model Token Usage"},"name":"model-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Model_Token_Usage\"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by model_name\n| order by total desc","size":0,"title":"Token Consumption by Model","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"green"}}]}},"customWidth":"50","name":"model-table"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Model_Token_Usage\"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by model_name\n| order by total desc","size":0,"title":"Token Distribution by Model","queryType":0,"resourceType":"microsoft.insights/components","visualization":"piechart"},"customWidth":"50","name":"model-chart"},{"type":1,"content":{"json":"## Per-Step (Team) Token Usage"},"name":"step-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Step_Token_Usage\"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc","size":0,"title":"Token Consumption by Workflow Step","queryType":0,"resourceType":"microsoft.insights/components","visualization":"barchart","chartSettings":{"xAxis":"step_name","yAxis":"total","group":"step_name"}},"customWidth":"50","name":"step-chart"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Step_Token_Usage\"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc","size":0,"title":"Step Usage Details","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"orange"}}]}},"customWidth":"50","name":"step-table"},{"type":1,"content":{"json":"## Per-User Token Usage"},"name":"user-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_tokens = toint(customDimensions.total_tokens),\n user_id = tostring(customDimensions.user_id)\n| summarize total = sum(total_tokens), runs = count() by user_id\n| order by total desc","size":0,"title":"Token Usage by User","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"purple"}}]}},"name":"user-table"},{"type":1,"content":{"json":"## Token Usage Trends"},"name":"trend-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage\"\n| where timestamp {TimeRange}\n| extend total_tokens = toint(customDimensions.total_tokens)\n| summarize hourly_tokens = sum(total_tokens), calls = count() by bin(timestamp, 1h)\n| order by timestamp asc","size":0,"title":"Hourly Token Consumption","queryType":0,"resourceType":"microsoft.insights/components","visualization":"linechart","chartSettings":{"xAxis":"timestamp","yAxis":"hourly_tokens","showLegend":true}},"name":"trend-chart"},{"type":1,"content":{"json":"## Estimated Cost\n\n> Cost estimates use GPT-4o pricing: **$2.50 / 1M input tokens**, **$10.00 / 1M output tokens**. Adjust for your actual model pricing."},"name":"cost-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n input_tokens = toint(customDimensions.total_input_tokens),\n output_tokens = toint(customDimensions.total_output_tokens)\n| extend estimated_cost_usd = round((input_tokens / 1000000.0 * 2.50) + (output_tokens / 1000000.0 * 10.0), 4)\n| project timestamp, process_id, input_tokens, output_tokens, estimated_cost_usd\n| order by estimated_cost_usd desc","size":0,"title":"Estimated Cost per Process (USD)","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"estimated_cost_usd","formatter":3,"formatOptions":{"palette":"redBright"}}]}},"name":"cost-table"},{"type":1,"content":{"json":"## Individual LLM Call Log"},"name":"calls-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n step_name = tostring(customDimensions.step_name),\n model = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n process_id = tostring(customDimensions.process_id)\n| project timestamp, process_id, agent_name, step_name, model, input_tokens, output_tokens, total_tokens\n| order by timestamp desc\n| take 200","size":0,"title":"Recent LLM Calls (last 200)","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total_tokens","formatter":3,"formatOptions":{"palette":"blue"}}]}},"name":"calls-table"}],"isLocked":false,"fallbackResourceIds":["/subscriptions/1d5876cd-7603-407a-96d2-ae5ca9a9c5f3/resourcegroups/rg-pricmglogp33/providers/microsoft.insights/components/appi-pricmglogp33usmqm"]} diff --git a/infra/dashboards/workbook-gke-content.json b/infra/dashboards/workbook-gke-content.json new file mode 100644 index 00000000..ad05834c --- /dev/null +++ b/infra/dashboards/workbook-gke-content.json @@ -0,0 +1 @@ +{"version":"Notebook/1.0","items":[{"type":1,"content":{"json":"# LLM Token Usage Dashboard\n\nThis workbook provides comprehensive visibility into LLM token consumption across agents, models, workflow steps, and users.\n\n---"},"name":"header"},{"type":9,"content":{"version":"KqlParameterItem/1.0","parameters":[{"id":"time-range-param","version":"KqlParameterItem/1.0","name":"TimeRange","type":4,"isRequired":true,"value":{"durationMs":1500000,"endTime":"2026-05-21T06:08:00.000Z"},"typeSettings":{"selectableValues":[{"durationMs":3600000},{"durationMs":14400000},{"durationMs":86400000},{"durationMs":259200000},{"durationMs":604800000},{"durationMs":2592000000}],"allowCustom":true},"label":"Time Range"}],"style":"pills","queryType":0,"resourceType":"microsoft.insights/components"},"name":"parameters"},{"type":1,"content":{"json":"## Overall Token Usage Summary"},"name":"summary-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| summarize \n total_input = sum(toint(customDimensions.total_input_tokens)),\n total_output = sum(toint(customDimensions.total_output_tokens)),\n total = sum(toint(customDimensions.total_tokens)),\n total_calls = sum(toint(customDimensions.total_calls)),\n processes = dcount(tostring(customDimensions.process_id))","size":4,"title":"Token Usage Totals","queryType":0,"resourceType":"microsoft.insights/components","visualization":"tiles","tileSettings":{"titleContent":{"columnMatch":"Column1","formatter":1},"leftContent":{"columnMatch":"total","formatter":12,"formatOptions":{"palette":"auto"},"numberFormat":{"unit":0,"options":{"style":"decimal","maximumFractionDigits":0}}},"showBorder":true}},"name":"summary-tiles"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_input = toint(customDimensions.total_input_tokens),\n total_output = toint(customDimensions.total_output_tokens),\n total = toint(customDimensions.total_tokens),\n call_count = toint(customDimensions.total_calls)\n| project timestamp, process_id, total_input, total_output, total, call_count\n| order by timestamp desc","size":0,"title":"Token Usage by Process","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"blue"}}]}},"name":"summary-table"},{"type":1,"content":{"json":"## Per-Agent Token Usage"},"name":"agent-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Agent_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by agent_name\n| order by total desc","size":0,"title":"Token Consumption by Agent","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"blue"}}]}},"customWidth":"50","name":"agent-table"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Agent_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by agent_name\n| order by total desc","size":0,"title":"Token Distribution by Agent","queryType":0,"resourceType":"microsoft.insights/components","visualization":"piechart"},"customWidth":"50","name":"agent-chart"},{"type":1,"content":{"json":"## Per-Model Token Usage"},"name":"model-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Model_Token_Usage\"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by model_name\n| order by total desc","size":0,"title":"Token Consumption by Model","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"green"}}]}},"customWidth":"50","name":"model-table"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Model_Token_Usage\"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by model_name\n| order by total desc","size":0,"title":"Token Distribution by Model","queryType":0,"resourceType":"microsoft.insights/components","visualization":"piechart"},"customWidth":"50","name":"model-chart"},{"type":1,"content":{"json":"## Per-Step (Team) Token Usage"},"name":"step-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Step_Token_Usage\"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc","size":0,"title":"Token Consumption by Workflow Step","queryType":0,"resourceType":"microsoft.insights/components","visualization":"barchart","chartSettings":{"xAxis":"step_name","yAxis":"total","group":"step_name"}},"customWidth":"50","name":"step-chart"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Step_Token_Usage\"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc","size":0,"title":"Step Usage Details","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"orange"}}]}},"customWidth":"50","name":"step-table"},{"type":1,"content":{"json":"## Per-User Token Usage"},"name":"user-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_tokens = toint(customDimensions.total_tokens),\n user_id = tostring(customDimensions.user_id)\n| summarize total = sum(total_tokens), runs = count() by user_id\n| order by total desc","size":0,"title":"Token Usage by User","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"purple"}}]}},"name":"user-table"},{"type":1,"content":{"json":"## Token Usage Trends"},"name":"trend-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage\"\n| where timestamp {TimeRange}\n| extend total_tokens = toint(customDimensions.total_tokens)\n| summarize hourly_tokens = sum(total_tokens), calls = count() by bin(timestamp, 1h)\n| order by timestamp asc","size":0,"title":"Hourly Token Consumption","queryType":0,"resourceType":"microsoft.insights/components","visualization":"linechart","chartSettings":{"xAxis":"timestamp","yAxis":"hourly_tokens","showLegend":true}},"name":"trend-chart"},{"type":1,"content":{"json":"## Estimated Cost\n\n> Cost estimates use GPT-4o pricing: **$2.50 / 1M input tokens**, **$10.00 / 1M output tokens**. Adjust for your actual model pricing."},"name":"cost-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n input_tokens = toint(customDimensions.total_input_tokens),\n output_tokens = toint(customDimensions.total_output_tokens)\n| extend estimated_cost_usd = round((input_tokens / 1000000.0 * 2.50) + (output_tokens / 1000000.0 * 10.0), 4)\n| project timestamp, process_id, input_tokens, output_tokens, estimated_cost_usd\n| order by estimated_cost_usd desc","size":0,"title":"Estimated Cost per Process (USD)","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"estimated_cost_usd","formatter":3,"formatOptions":{"palette":"redBright"}}]}},"name":"cost-table"},{"type":1,"content":{"json":"## Individual LLM Call Log"},"name":"calls-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n step_name = tostring(customDimensions.step_name),\n model = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n process_id = tostring(customDimensions.process_id)\n| project timestamp, process_id, agent_name, step_name, model, input_tokens, output_tokens, total_tokens\n| order by timestamp desc\n| take 200","size":0,"title":"Recent LLM Calls (last 200)","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total_tokens","formatter":3,"formatOptions":{"palette":"blue"}}]}},"name":"calls-table"}],"isLocked":false,"fallbackResourceIds":["/subscriptions/1d5876cd-7603-407a-96d2-ae5ca9a9c5f3/resourcegroups/rg-pricmglogp33/providers/microsoft.insights/components/appi-pricmglogp33usmqm"]} diff --git a/infra/main.bicep b/infra/main.bicep index 904fee03..3306c57a 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -91,7 +91,7 @@ param enableTelemetry bool = true param enablePrivateNetworking bool = false @description('Optional. Enable monitoring applicable resources, aligned with the Well Architected Framework recommendations. This setting enables Application Insights and Log Analytics and configures all the resources applicable resources to send logs. Defaults to false.') -param enableMonitoring bool = false +param enableMonitoring bool = true @description('Optional. Enable scalability for applicable resources, aligned with the Well Architected Framework recommendations. Defaults to false.') param enableScalability bool = false @@ -310,6 +310,16 @@ module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (en } } +// ========== LLM Token Usage Workbook ========== // +module tokenUsageWorkbook './modules/tokenUsageWorkbook.bicep' = if (enableMonitoring) { + name: take('module.token-usage-workbook.${solutionSuffix}', 64) + params: { + location: solutionLocation + applicationInsightsResourceId: applicationInsights!.outputs.resourceId + tags: allTags + } +} + // ========== Virtual Network ========== // module virtualNetwork './modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { name: take('module.virtual-network.${solutionSuffix}', 64) diff --git a/infra/main.parameters.json b/infra/main.parameters.json index b4a1a7cc..6df293a4 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -26,11 +26,17 @@ "gptDeploymentCapacity": { "value": "${AZURE_ENV_GPT_MODEL_CAPACITY}" }, - "existingLogAnalyticsWorkspaceId": { - "value": "${AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID}" + "enableTelemetry": { + "value": true }, - "existingFoundryProjectResourceId": { - "value": "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}" + "enableMonitoring": { + "value": true + }, + "enablePrivateNetworking": { + "value": false + }, + "enableScalability": { + "value": false }, "vmAdminUsername": { "value": "${AZURE_ENV_VM_ADMIN_USERNAME}" @@ -38,8 +44,17 @@ "vmAdminPassword": { "value": "${AZURE_ENV_VM_ADMIN_PASSWORD}" }, + "existingLogAnalyticsWorkspaceId": { + "value": "${AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID}" + }, + "existingFoundryProjectResourceId": { + "value": "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}" + }, "imageTag": { "value": "${AZURE_ENV_IMAGE_TAG}" + }, + "vmSize": { + "value": "${AZURE_ENV_VM_SIZE}" } } } diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index f93b93ab..9e106e59 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -84,7 +84,7 @@ param enableTelemetry bool = true param enablePrivateNetworking bool = false @description('Optional. Enable monitoring applicable resources, aligned with the Well Architected Framework recommendations. This setting enables Application Insights and Log Analytics and configures all the resources applicable resources to send logs. Defaults to false.') -param enableMonitoring bool = false +param enableMonitoring bool = true @description('Optional. Enable scalability for applicable resources, aligned with the Well Architected Framework recommendations. Defaults to false.') param enableScalability bool = false @@ -288,6 +288,16 @@ module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (en } } +// ========== LLM Token Usage Workbook ========== // +module tokenUsageWorkbook './modules/tokenUsageWorkbook.bicep' = if (enableMonitoring) { + name: take('module.token-usage-workbook.${solutionSuffix}', 64) + params: { + location: solutionLocation + applicationInsightsResourceId: applicationInsights!.outputs.resourceId + tags: allTags + } +} + // ========== Virtual Network ========== // module virtualNetwork './modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { name: take('module.virtual-network.${solutionSuffix}', 64) diff --git a/infra/modules/tokenUsageWorkbook.bicep b/infra/modules/tokenUsageWorkbook.bicep new file mode 100644 index 00000000..6531bdda --- /dev/null +++ b/infra/modules/tokenUsageWorkbook.bicep @@ -0,0 +1,458 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +@description('Required. The location for the workbook resource.') +param location string + +@description('Required. The resource ID of the Application Insights instance to query.') +param applicationInsightsResourceId string + +@description('Optional. Tags to apply to the workbook resource.') +param tags object = {} + +@description('Optional. Display name for the workbook.') +param workbookDisplayName string = 'LLM Token Usage Dashboard' + +// Generate a deterministic GUID for the workbook based on resource group and name +var workbookId = guid(resourceGroup().id, 'token-usage-workbook') + +var workbookContent = { + version: 'Notebook/1.0' + items: [ + { + type: 1 + content: { + json: '# LLM Token Usage Dashboard\n\nThis workbook provides comprehensive visibility into LLM token consumption across agents, models, workflow steps, and users.\n\n---' + } + name: 'header' + } + { + type: 9 + content: { + version: 'KqlParameterItem/1.0' + parameters: [ + { + id: 'time-range-param' + version: 'KqlParameterItem/1.0' + name: 'TimeRange' + type: 4 + isRequired: true + value: { + durationMs: 86400000 + } + typeSettings: { + selectableValues: [ + { durationMs: 3600000 } + { durationMs: 14400000 } + { durationMs: 86400000 } + { durationMs: 259200000 } + { durationMs: 604800000 } + { durationMs: 2592000000 } + ] + allowCustom: true + } + label: 'Time Range' + } + ] + style: 'pills' + queryType: 0 + resourceType: 'microsoft.insights/components' + } + name: 'parameters' + } + // ===== Row 1: Summary Tiles ===== + { + type: 1 + content: { + json: '## Overall Token Usage Summary' + } + name: 'summary-header' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Token_Usage_Summary"\n| where timestamp {TimeRange}\n| summarize \n total_input = sum(toint(customDimensions.total_input_tokens)),\n total_output = sum(toint(customDimensions.total_output_tokens)),\n total = sum(toint(customDimensions.total_tokens)),\n total_calls = sum(toint(customDimensions.total_calls)),\n processes = dcount(tostring(customDimensions.process_id))' + size: 4 + title: 'Token Usage Totals' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'tiles' + tileSettings: { + titleContent: { + columnMatch: 'Column1' + formatter: 1 + } + leftContent: { + columnMatch: 'total' + formatter: 12 + formatOptions: { + palette: 'auto' + } + numberFormat: { + unit: 0 + options: { + style: 'decimal' + maximumFractionDigits: 0 + } + } + } + showBorder: true + } + } + name: 'summary-tiles' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Token_Usage_Summary"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_input = toint(customDimensions.total_input_tokens),\n total_output = toint(customDimensions.total_output_tokens),\n total = toint(customDimensions.total_tokens),\n call_count = toint(customDimensions.total_calls)\n| project timestamp, process_id, total_input, total_output, total, call_count\n| order by timestamp desc' + size: 0 + title: 'Token Usage by Process' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'table' + gridSettings: { + formatters: [ + { + columnMatch: 'total' + formatter: 3 + formatOptions: { + palette: 'blue' + } + } + ] + } + } + name: 'summary-table' + } + // ===== Row 2: Per-Agent Token Usage ===== + { + type: 1 + content: { + json: '## Per-Agent Token Usage' + } + name: 'agent-header' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Agent_Token_Usage"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by agent_name\n| order by total desc' + size: 0 + title: 'Token Consumption by Agent' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'table' + gridSettings: { + formatters: [ + { + columnMatch: 'total' + formatter: 3 + formatOptions: { + palette: 'blue' + } + } + ] + } + } + customWidth: '50' + name: 'agent-table' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Agent_Token_Usage"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by agent_name\n| order by total desc' + size: 0 + title: 'Token Distribution by Agent' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'piechart' + } + customWidth: '50' + name: 'agent-chart' + } + // ===== Row 3: Per-Model Token Usage ===== + { + type: 1 + content: { + json: '## Per-Model Token Usage' + } + name: 'model-header' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Model_Token_Usage"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by model_name\n| order by total desc' + size: 0 + title: 'Token Consumption by Model' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'table' + gridSettings: { + formatters: [ + { + columnMatch: 'total' + formatter: 3 + formatOptions: { + palette: 'green' + } + } + ] + } + } + customWidth: '50' + name: 'model-table' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Model_Token_Usage"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by model_name\n| order by total desc' + size: 0 + title: 'Token Distribution by Model' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'piechart' + } + customWidth: '50' + name: 'model-chart' + } + // ===== Row 4: Per-Step (Team) Token Usage ===== + { + type: 1 + content: { + json: '## Per-Step (Team) Token Usage' + } + name: 'step-header' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Step_Token_Usage"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc' + size: 0 + title: 'Token Consumption by Workflow Step' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'barchart' + chartSettings: { + xAxis: 'step_name' + yAxis: 'total' + group: 'step_name' + } + } + customWidth: '50' + name: 'step-chart' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Step_Token_Usage"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc' + size: 0 + title: 'Step Usage Details' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'table' + gridSettings: { + formatters: [ + { + columnMatch: 'total' + formatter: 3 + formatOptions: { + palette: 'orange' + } + } + ] + } + } + customWidth: '50' + name: 'step-table' + } + // ===== Row 5: Per-User Token Usage ===== + { + type: 1 + content: { + json: '## Per-User Token Usage' + } + name: 'user-header' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Token_Usage_Summary"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_tokens = toint(customDimensions.total_tokens),\n user_id = tostring(customDimensions.user_id)\n| summarize total = sum(total_tokens), runs = count() by user_id\n| order by total desc' + size: 0 + title: 'Token Usage by User' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'table' + gridSettings: { + formatters: [ + { + columnMatch: 'total' + formatter: 3 + formatOptions: { + palette: 'purple' + } + } + ] + } + } + name: 'user-table' + } + // ===== Row 6: Hourly Token Usage Trend ===== + { + type: 1 + content: { + json: '## Token Usage Trends' + } + name: 'trend-header' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Token_Usage"\n| where timestamp {TimeRange}\n| extend total_tokens = toint(customDimensions.total_tokens)\n| summarize hourly_tokens = sum(total_tokens), calls = count() by bin(timestamp, 1h)\n| order by timestamp asc' + size: 0 + title: 'Hourly Token Consumption' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'linechart' + chartSettings: { + xAxis: 'timestamp' + yAxis: 'hourly_tokens' + showLegend: true + } + } + name: 'trend-chart' + } + // ===== Row 7: Estimated Cost ===== + { + type: 1 + content: { + json: '## Estimated Cost\n\n> Cost estimates use GPT-4o pricing: **$2.50 / 1M input tokens**, **$10.00 / 1M output tokens**. Adjust for your actual model pricing.' + } + name: 'cost-header' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Token_Usage_Summary"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n input_tokens = toint(customDimensions.total_input_tokens),\n output_tokens = toint(customDimensions.total_output_tokens)\n| extend estimated_cost_usd = round((input_tokens / 1000000.0 * 2.50) + (output_tokens / 1000000.0 * 10.0), 4)\n| project timestamp, process_id, input_tokens, output_tokens, estimated_cost_usd\n| order by estimated_cost_usd desc' + size: 0 + title: 'Estimated Cost per Process (USD)' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'table' + gridSettings: { + formatters: [ + { + columnMatch: 'estimated_cost_usd' + formatter: 3 + formatOptions: { + palette: 'redBright' + } + } + ] + } + } + name: 'cost-table' + } + // ===== Row 8: Individual LLM Call Log ===== + { + type: 1 + content: { + json: '## Individual LLM Call Log' + } + name: 'calls-header' + } + { + type: 3 + content: { + version: 'KqlItem/1.0' + query: 'customEvents\n| where name == "LLM_Token_Usage"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n step_name = tostring(customDimensions.step_name),\n model = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n process_id = tostring(customDimensions.process_id)\n| project timestamp, process_id, agent_name, step_name, model, input_tokens, output_tokens, total_tokens\n| order by timestamp desc\n| take 200' + size: 0 + title: 'Recent LLM Calls (last 200)' + queryType: 0 + resourceType: 'microsoft.insights/components' + crossComponentResources: [ + applicationInsightsResourceId + ] + visualization: 'table' + gridSettings: { + formatters: [ + { + columnMatch: 'total_tokens' + formatter: 3 + formatOptions: { + palette: 'blue' + } + } + ] + } + } + name: 'calls-table' + } + ] + isLocked: false + fallbackResourceIds: [ + applicationInsightsResourceId + ] +} + +resource tokenUsageWorkbook 'Microsoft.Insights/workbooks@2023-06-01' = { + name: workbookId + location: location + tags: tags + kind: 'shared' + properties: { + displayName: workbookDisplayName + category: 'workbook' + version: '1.0' + serializedData: string(workbookContent) + sourceId: applicationInsightsResourceId + } +} + +@description('The resource ID of the created workbook.') +output resourceId string = tokenUsageWorkbook.id + +@description('The name of the created workbook.') +output name string = tokenUsageWorkbook.name diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index 0ad16303..c7c40439 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -13,6 +13,7 @@ RUN npm install COPY . . # Build the app +ENV NODE_OPTIONS="--max-old-space-size=4096" RUN npm run build # Runtime stage diff --git a/src/processor/Dockerfile b/src/processor/Dockerfile index afe293a5..317c8e03 100644 --- a/src/processor/Dockerfile +++ b/src/processor/Dockerfile @@ -36,7 +36,8 @@ RUN curl -fsSLo /tmp/node.tar.gz "https://nodejs.org/dist/v${NODE_VERSION}/node- COPY pyproject.toml uv.lock ./ # Install dependencies using UV -RUN uv sync --frozen --python 3.12 +# Re-lock to pick up any pyproject.toml changes (e.g. new deps), then install. +RUN uv lock --python 3.12 && uv sync --frozen --python 3.12 # Copy the entire source code COPY src/ ./src/ diff --git a/src/processor/pyproject.toml b/src/processor/pyproject.toml index 846621b5..80a0768a 100644 --- a/src/processor/pyproject.toml +++ b/src/processor/pyproject.toml @@ -13,6 +13,8 @@ dependencies = [ "azure-ai-projects==2.0.0b3", "azure-appconfiguration==1.7.2", "azure-core==1.38.0", + "azure-monitor-events-extension==0.1.0", + "azure-monitor-opentelemetry==1.8.7", "azure-cosmos==4.15.0", "azure-identity==1.26.0b1", "azure-storage-blob==12.28.0", diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 5851b809..bc7d4e4b 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -23,6 +23,140 @@ logger = logging.getLogger(__name__) +def _extract_tokens_from_dict_or_obj(ud: Any) -> tuple[int, int, int]: + """Extract (input, output, total) token counts from a dict or object.""" + inp = out = tot = 0 + if isinstance(ud, dict): + inp = ud.get("input_token_count", 0) or ud.get("input_tokens", 0) or 0 + out = ud.get("output_token_count", 0) or ud.get("output_tokens", 0) or 0 + tot = ud.get("total_token_count", 0) or ud.get("total_tokens", 0) or 0 + else: + inp = getattr(ud, "input_token_count", 0) or getattr(ud, "input_tokens", 0) or 0 + out = getattr(ud, "output_token_count", 0) or getattr(ud, "output_tokens", 0) or 0 + tot = getattr(ud, "total_token_count", 0) or getattr(ud, "total_tokens", 0) or 0 + if not tot: + tot = int(inp) + int(out) + return int(inp), int(out), int(tot) + + +def _try_emit_token_event(inp: int, out: int, tot: int, source: str) -> None: + """Log token usage found in stream/response for diagnostics. + + The actual LLM_Token_Usage event is emitted by TokenUsageTracker.record() + in the orchestrator, which has full context (agent, step, model, user). + This function only logs for debugging to avoid duplicate events. + """ + if tot > 0 or inp > 0 or out > 0: + logger.info( + "[TOKEN_STREAM] usage found: input=%s output=%s total=%s source=%s", + inp, out, tot, source, + ) + + +def _emit_usage_from_stream_item(item: Any) -> None: + """Check a streamed ChatResponseUpdate for usage Content and emit an App Insights event. + + Checks multiple locations where usage data may appear: + 1. item.contents[] with type="usage" and usage_details + 2. item.usage (direct attribute - some SDK versions) + 3. item.metadata with usage keys + """ + try: + item_type = type(item).__name__ + + # --- Path 1: contents list with Content(type="usage") --- + contents = getattr(item, "contents", None) + if contents: + for content in contents: + ctype = getattr(content, "type", None) + if ctype == "usage": + # SDK UsageContent uses "details"; fall back to "usage_details" + ud = getattr(content, "details", None) or getattr(content, "usage_details", None) + if ud: + inp, out, tot = _extract_tokens_from_dict_or_obj(ud) + _try_emit_token_event(inp, out, tot, "stream_contents") + return + + # --- Path 2: direct .usage attribute --- + usage = getattr(item, "usage", None) + if usage is not None: + inp, out, tot = _extract_tokens_from_dict_or_obj(usage) + _try_emit_token_event(inp, out, tot, "stream_usage_attr") + return + + # --- Path 3: .metadata dict with usage keys --- + metadata = getattr(item, "metadata", None) + if isinstance(metadata, dict): + if any(k in metadata for k in ("input_tokens", "input_token_count", "usage")): + usage_data = metadata.get("usage", metadata) + inp, out, tot = _extract_tokens_from_dict_or_obj(usage_data) + _try_emit_token_event(inp, out, tot, "stream_metadata") + return + + # --- Diagnostic: log item shape for debugging (only for non-text items) --- + if contents: + content_types = [getattr(c, "type", "?") for c in contents] + if any(t not in ("text",) for t in content_types): + logger.debug( + "[TOKEN_DIAG] item_type=%s content_types=%s attrs=%s", + item_type, + content_types, + [a for a in dir(item) if not a.startswith("_")], + ) + except Exception as e: + logger.debug("[TOKEN_STREAM] error in emit: %s", e) + + +def _emit_usage_from_response(response: Any) -> None: + """Extract and emit token usage from a non-streaming ChatResponse. + + Checks usage_details (SDK attribute) and contents for UsageContent items. + """ + try: + # Path 1: response.usage_details (ChatResponse from SDK) + ud = getattr(response, "usage_details", None) or getattr(response, "details", None) + if ud is not None: + inp, out, tot = _extract_tokens_from_dict_or_obj(ud) + _try_emit_token_event(inp, out, tot, "response_usage_details") + return + + # Path 2: response.usage direct attribute + usage = getattr(response, "usage", None) + if usage is not None: + inp, out, tot = _extract_tokens_from_dict_or_obj(usage) + _try_emit_token_event(inp, out, tot, "response_usage_attr") + return + + # Path 3: contents list with UsageContent + contents = getattr(response, "contents", None) + if contents: + for content in contents: + ctype = getattr(content, "type", None) + if ctype == "usage": + ud = getattr(content, "details", None) or getattr(content, "usage_details", None) + if ud: + inp, out, tot = _extract_tokens_from_dict_or_obj(ud) + _try_emit_token_event(inp, out, tot, "response_contents") + return + + # Path 4: messages list with usage content + messages = getattr(response, "messages", None) + if messages: + for msg in messages: + msg_contents = getattr(msg, "contents", None) + if not msg_contents: + continue + for item in msg_contents: + if getattr(item, "type", None) == "usage": + ud = getattr(item, "details", None) or getattr(item, "usage_details", None) + if ud: + inp, out, tot = _extract_tokens_from_dict_or_obj(ud) + _try_emit_token_event(inp, out, tot, "response_msg_contents") + return + except Exception as e: + logger.debug("[TOKEN_RESPONSE] error in emit: %s", e) + + def _format_exc_brief(exc: BaseException) -> str: name = type(exc).__name__ msg = str(exc) @@ -78,6 +212,20 @@ def _looks_like_rate_limit(error: BaseException) -> bool: if isinstance(status, int) and 500 <= status < 600: return True + # "The model produced invalid content" is a transient error from Azure OpenAI + # when the model output fails content/schema validation — worth retrying. + # "No tool call found" is a 400 error when the conversation has orphaned + # function call outputs with no matching tool call request. + if any( + s in msg + for s in [ + "model produced invalid content", + "invalid content", + "no tool call found", + ] + ): + return True + cause = getattr(error, "__cause__", None) if cause and cause is not error: return _looks_like_rate_limit(cause) @@ -548,12 +696,15 @@ async def _inner_get_response( ) try: - return await _retry_call( + response = await _retry_call( lambda: parent_inner_get_response( messages=effective_messages, chat_options=chat_options, **kwargs ), config=self._retry_config, ) + # Extract and emit token usage from non-streaming response + _emit_usage_from_response(response) + return response except Exception as e: if not ( self._context_trim_config.enabled @@ -643,8 +794,36 @@ async def _tail(): async for item in iterator: yield item + _item_count = 0 + _last_item = None async for item in _tail(): + _item_count += 1 + _last_item = item + _emit_usage_from_stream_item(item) yield item + + # After stream completes, log diagnostic about the last item + if _last_item is not None: + try: + _attrs = [a for a in dir(_last_item) if not a.startswith("_")] + _contents = getattr(_last_item, "contents", None) + _content_info = [] + if _contents: + for _c in _contents: + _ct = getattr(_c, "type", "?") + _ca = [a for a in dir(_c) if not a.startswith("_")] + _content_info.append({"type": _ct, "attrs": _ca}) + _usage_attr = getattr(_last_item, "usage", None) + logger.info( + "[TOKEN_DIAG_FINAL] stream_items=%d last_item_type=%s attrs=%s contents=%s usage_attr=%s", + _item_count, + type(_last_item).__name__, + _attrs, + _content_info, + repr(_usage_attr) if _usage_attr is not None else "None", + ) + except Exception: + pass return except StopAsyncIteration: return diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 5cb63938..50711fd3 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -36,6 +36,8 @@ from mem0 import AsyncMemory from pydantic import BaseModel, ValidationError +from utils.token_usage_tracker import TokenUsageTracker, extract_usage_from_response, _parse_usage_object + logger = logging.getLogger(__name__) @@ -93,6 +95,7 @@ class OrchestrationResult(Generic[TOutput]): result: TOutput | None = None error: str | None = None execution_time_seconds: float = 0.0 + token_usage_summary: dict[str, Any] | None = None @staticmethod def _to_jsonable(value: Any) -> Any: @@ -156,6 +159,7 @@ def model_dump(self) -> dict[str, Any]: "result": self._to_jsonable(self.result), "error": self.error, "execution_time_seconds": self.execution_time_seconds, + "token_usage_summary": self.token_usage_summary, } def to_json(self, *, indent: int = 2) -> str: @@ -195,6 +199,7 @@ def __init__( max_rounds: int = 100, max_seconds: float | None = None, result_output_format: type[TOutput] | None = None, + token_usage_tracker: TokenUsageTracker | None = None, ): """ Initialize the orchestrator. @@ -224,11 +229,15 @@ def __init__( self.max_seconds = max_seconds self.result_format = result_output_format + # Token usage tracker (optional — provided by OrchestratorBase) + self.token_usage_tracker = token_usage_tracker + # Runtime state self.agents: dict[str, ChatAgent] = participants self.agent_tool_usage: dict[str, list[dict[str, Any]]] = {} self.agent_responses: list[AgentResponse] = [] self._initialized: bool = False + self._streaming_captured_usage: bool = False # Streaming response buffer self._last_executor_id: str | None = None @@ -546,6 +555,11 @@ async def run_stream( # items inside ChatMessage.contents. self._backfill_tool_usage_from_conversation(conversation) + # Backfill token usage from conversation messages. + # Streaming events may not surface usage Content items, but the final + # conversation messages reliably carry them. + self._backfill_token_usage_from_conversation(conversation) + # Post-workflow analysis (optional) final_analysis = None result_format = self.result_format @@ -606,6 +620,11 @@ async def run_stream( execution_time = (datetime.now() - start_time).total_seconds() # Build result + # Collect token usage summary if tracker is active + token_summary = None + if self.token_usage_tracker is not None: + token_summary = self.token_usage_tracker.get_summary() + result = OrchestrationResult[TOutput]( success=True, conversation=conversation, @@ -614,6 +633,7 @@ async def run_stream( result=final_analysis, error=None, execution_time_seconds=execution_time, + token_usage_summary=token_summary, ) # Callback for completion with Typed Result @@ -625,6 +645,10 @@ async def run_stream( except Exception as e: execution_time = (datetime.now() - start_time).total_seconds() + token_summary = None + if self.token_usage_tracker is not None: + token_summary = self.token_usage_tracker.get_summary() + error_result = OrchestrationResult[TOutput]( success=False, conversation=[], @@ -633,6 +657,7 @@ async def run_stream( result=None, error=str(e), execution_time_seconds=execution_time, + token_usage_summary=token_summary, ) if on_workflow_complete: @@ -660,6 +685,78 @@ async def _handle_agent_update( self._append_text_chunk(event) await self._process_tool_calls(event, agent_name, stream_callback) + # Extract token usage from the streaming update if tracker is active. + # Check multiple paths where usage data may appear. + if self.token_usage_tracker is not None: + try: + data = event.data + # Path 1: data.contents with Content(type="usage") + contents = getattr(data, "contents", None) + if contents: + for item in contents: + ctype = getattr(item, "type", None) + if ctype == "usage": + # SDK UsageContent uses "details"; fall back to "usage_details" + ud = getattr(item, "details", None) or getattr(item, "usage_details", None) + if ud: + record = _parse_usage_object(ud) + if record and record.total_tokens > 0: + self.token_usage_tracker.record( + input_tokens=record.input_tokens, + output_tokens=record.output_tokens, + total_tokens=record.total_tokens, + agent_name=agent_name, + step_name=self.name, + ) + self._streaming_captured_usage = True + logger.info( + "[TOKEN_ORCH] recorded from contents: agent=%s step=%s tokens=%s", + agent_name, self.name, record.total_tokens, + ) + return + # Path 2: data.usage direct attribute + usage = getattr(data, "usage", None) + if usage is not None: + record = _parse_usage_object(usage) + if record and record.total_tokens > 0: + self.token_usage_tracker.record( + input_tokens=record.input_tokens, + output_tokens=record.output_tokens, + total_tokens=record.total_tokens, + agent_name=agent_name, + step_name=self.name, + ) + self._streaming_captured_usage = True + logger.info( + "[TOKEN_ORCH] recorded from usage attr: agent=%s step=%s tokens=%s", + agent_name, self.name, record.total_tokens, + ) + return + # Path 3: event itself may carry usage + event_usage = getattr(event, "usage", None) + if event_usage is not None: + record = _parse_usage_object(event_usage) + if record and record.total_tokens > 0: + self.token_usage_tracker.record( + input_tokens=record.input_tokens, + output_tokens=record.output_tokens, + total_tokens=record.total_tokens, + agent_name=agent_name, + step_name=self.name, + ) + self._streaming_captured_usage = True + logger.info( + "[TOKEN_ORCH] recorded from event.usage: agent=%s step=%s tokens=%s", + agent_name, self.name, record.total_tokens, + ) + return + except Exception: + logger.debug( + "Failed to extract token usage from update (agent=%s)", + agent_name, + exc_info=True, + ) + def _normalize_executor_id(self, executor_id: str) -> str: """Normalize executor id to agent name. @@ -930,6 +1027,73 @@ def _backfill_tool_usage_from_conversation( # Best effort only; don't break orchestration continue + def _backfill_token_usage_from_conversation( + self, conversation: list[ChatMessage] + ) -> None: + """Extract token usage from the final conversation messages. + + The agent_framework attaches ``Content(type="usage")`` items to + assistant messages when the underlying LLM response completes. + Streaming updates may not surface these, so we scan the final + conversation as a **fallback only when streaming did not capture + any usage** to avoid double-counting. + """ + if self.token_usage_tracker is None: + return + + # Skip backfill if streaming already captured token usage for this orchestrator run. + if getattr(self, "_streaming_captured_usage", False): + logger.info( + "[TOKEN] Skipping backfill — streaming already captured usage (step=%s)", + self.name, + ) + return + + found_any = False + for msg in conversation: + try: + role = getattr(msg, "role", None) + author = getattr(msg, "author_name", None) or "unknown" + contents = getattr(msg, "contents", None) + if not contents: + continue + + for item in contents: + item_type = getattr(item, "type", None) + if item_type != "usage": + continue + + # SDK UsageContent uses "details"; fall back to "usage_details" + ud = getattr(item, "details", None) or getattr(item, "usage_details", None) + if ud is None: + continue + + record = _parse_usage_object(ud) + if record and record.total_tokens > 0: + agent_name = self._normalize_executor_id(author) + self.token_usage_tracker.record( + input_tokens=record.input_tokens, + output_tokens=record.output_tokens, + total_tokens=record.total_tokens, + agent_name=agent_name, + step_name=self.name, + ) + found_any = True + except Exception: + continue + + if found_any: + logger.info( + "[TOKEN] Backfilled token usage from conversation (step=%s)", + self.name, + ) + else: + logger.warning( + "[TOKEN] No usage Content found in conversation messages (step=%s, msgs=%d)", + self.name, + len(conversation), + ) + async def _complete_agent_response( self, agent_id: str, diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index 46dce8c6..cfeb8a04 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -26,6 +26,7 @@ ) from utils.agent_telemetry import TelemetryManager from utils.console_util import format_agent_message +from utils.token_usage_tracker import TokenUsageTracker from .agent_base import AgentBase @@ -42,6 +43,7 @@ def __init__(self, app_context=None): self.initialized = False self.memory_store: QdrantMemoryStore | None = None self.step_name: str = "" + self.token_tracker: TokenUsageTracker | None = None def is_console_summarization_enabled(self) -> bool: """Return True if console summarization (extra LLM call per turn) is enabled. @@ -83,6 +85,23 @@ async def initialize(self, process_id: str): self.agents = await self.create_agents(self.agentinfos, process_id=process_id) self.initialized = True + # Resolve workflow-level token usage tracker from AppContext (if registered) + if self.app_context.is_registered(TokenUsageTracker): + try: + self.token_tracker = self.app_context.get_service(TokenUsageTracker) + # Register model deployment name for all agents so per-model tracking works + try: + deployment_name = self.agent_framework_helper.settings.get_service_config( + "default" + ).chat_deployment_name + if deployment_name and self.token_tracker: + for agent_name in (self.agents or {}): + self.token_tracker.set_agent_model(agent_name, deployment_name) + except Exception: + logger.debug("Could not register agent-model mapping", exc_info=True) + except Exception: + self.token_tracker = None + async def flush_agent_memories(self) -> None: """Flush buffered memories from all agent context providers. @@ -188,10 +207,12 @@ async def create_agents( ) elif agent_info.agent_name == "ResultGenerator": # Structured JSON generation; deterministic and bounded. + # Use 25_000 to prevent truncation of complex nested JSON schemas + # which causes "model produced invalid content" errors. builder = ( builder .with_temperature(0.0) - .with_max_tokens(12_000) + .with_max_tokens(25_000) .with_tool_choice("none") ) diff --git a/src/processor/src/main.py b/src/processor/src/main.py index 79531ff4..0c7252ab 100644 --- a/src/processor/src/main.py +++ b/src/processor/src/main.py @@ -45,8 +45,37 @@ def initialize(self): self.application_context.configuration, ) + self._configure_azure_monitor() self.register_services() + def _configure_azure_monitor(self): + """Initialise Azure Monitor OpenTelemetry exporter, if configured.""" + connection_string = os.environ.get( + "APPLICATIONINSIGHTS_CONNECTION_STRING", "" + ).strip() + if not connection_string: + logger.info( + "APPLICATIONINSIGHTS_CONNECTION_STRING not set; " + "skipping Azure Monitor OpenTelemetry configuration." + ) + return + + try: + from azure.monitor.opentelemetry import configure_azure_monitor + + configure_azure_monitor( + connection_string=connection_string, + enable_live_metrics=True, + ) + logger.info( + "Azure Monitor OpenTelemetry configured (live metrics enabled)." + ) + except Exception: + logger.exception( + "Failed to configure Azure Monitor OpenTelemetry; " + "continuing without App Insights export." + ) + def register_services(self): self.application_context.add_singleton( AgentFrameworkHelper, AgentFrameworkHelper() diff --git a/src/processor/src/main_service.py b/src/processor/src/main_service.py index 3b346161..88191403 100644 --- a/src/processor/src/main_service.py +++ b/src/processor/src/main_service.py @@ -105,8 +105,41 @@ def initialize(self): "Application initialized with configuration: %s", self.application_context.configuration, ) + self._configure_azure_monitor() self.register_services() + def _configure_azure_monitor(self): + """Initialise Azure Monitor OpenTelemetry exporter, if configured. + + Required so that ``track_event`` from ``azure-monitor-events-extension`` + has an export pipeline for custom events (e.g. token usage tracking). + """ + connection_string = os.environ.get( + "APPLICATIONINSIGHTS_CONNECTION_STRING", "" + ).strip() + if not connection_string: + logger.info( + "APPLICATIONINSIGHTS_CONNECTION_STRING not set; " + "skipping Azure Monitor OpenTelemetry configuration." + ) + return + + try: + from azure.monitor.opentelemetry import configure_azure_monitor + + configure_azure_monitor( + connection_string=connection_string, + enable_live_metrics=True, + ) + logger.info( + "Azure Monitor OpenTelemetry configured (live metrics enabled)." + ) + except Exception: + logger.exception( + "Failed to configure Azure Monitor OpenTelemetry; " + "continuing without App Insights export." + ) + def register_services(self): """Register application services into the dependency injection container. diff --git a/src/processor/src/services/queue_service.py b/src/processor/src/services/queue_service.py index a6b1be1e..0c65a30f 100644 --- a/src/processor/src/services/queue_service.py +++ b/src/processor/src/services/queue_service.py @@ -1272,6 +1272,7 @@ def _build_task_param(self, queue_message: QueueMessage) -> Analysis_TaskParam: source_file_folder=req["source_file_folder"], workspace_file_folder=req["workspace_file_folder"], output_file_folder=req["output_file_folder"], + user_id=parsed.user_id or req.get("user_id", ""), ) async def _ensure_queues_exist(self): diff --git a/src/processor/src/steps/analysis/models/step_param.py b/src/processor/src/steps/analysis/models/step_param.py index ca358049..3ed75f9a 100644 --- a/src/processor/src/steps/analysis/models/step_param.py +++ b/src/processor/src/steps/analysis/models/step_param.py @@ -16,3 +16,4 @@ class Analysis_TaskParam(BaseModel): source_file_folder: str = Field(description="Path to the source files folder") output_file_folder: str = Field(description="Path to the output files folder") workspace_file_folder: str = Field(description="Path to the workspace files folder") + user_id: str = Field(default="", description="User identifier for token usage tracking") diff --git a/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py b/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py index 93f8f2f0..5a182b04 100644 --- a/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py +++ b/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py @@ -84,6 +84,7 @@ async def execute( participants=self.agents, memory_client=None, result_output_format=Analysis_BooleanExtendedResult, + token_usage_tracker=self.token_tracker, ) orchestration_result = await orchestrator.run_stream( diff --git a/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py b/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py index f1fe8b4d..f2ee60ca 100644 --- a/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py +++ b/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py @@ -93,6 +93,7 @@ async def execute( memory_client=None, max_seconds=900, result_output_format=Yaml_ExtendedBooleanResult, + token_usage_tracker=self.token_tracker, ) orchestration_result = await orchestrator.run_stream( diff --git a/src/processor/src/steps/design/orchestration/design_orchestrator.py b/src/processor/src/steps/design/orchestration/design_orchestrator.py index d2dd47f0..68fea0c5 100644 --- a/src/processor/src/steps/design/orchestration/design_orchestrator.py +++ b/src/processor/src/steps/design/orchestration/design_orchestrator.py @@ -84,6 +84,7 @@ async def execute( participants=self.agents, memory_client=None, result_output_format=Design_ExtendedBooleanResult, + token_usage_tracker=self.token_tracker, ) orchestration_result = await orchestrator.run_stream( diff --git a/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py b/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py index 0aa6c443..cb995f91 100644 --- a/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py +++ b/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py @@ -98,6 +98,7 @@ async def execute( participants=self.agents, memory_client=None, result_output_format=Documentation_ExtendedBooleanResult, + token_usage_tracker=self.token_tracker, ) orchestration_result = await orchestrator.run_stream( diff --git a/src/processor/src/steps/migration_processor.py b/src/processor/src/steps/migration_processor.py index 0ded130f..549038e3 100644 --- a/src/processor/src/steps/migration_processor.py +++ b/src/processor/src/steps/migration_processor.py @@ -56,6 +56,7 @@ from libs.reporting.models.failure_context import FailureType from utils.agent_telemetry import TelemetryManager from utils.credential_util import get_bearer_token_provider +from utils.token_usage_tracker import TokenUsageTracker from .analysis.models.step_param import Analysis_TaskParam from .analysis.workflow.analysis_executor import AnalysisExecutor @@ -297,6 +298,14 @@ async def run(self, input_data: Analysis_TaskParam) -> Any: self.app_context._instances.pop(QdrantMemoryStore, None) self.app_context.add_singleton(QdrantMemoryStore, memory_store) + # Create workflow-level token usage tracker and register in app context + token_tracker = TokenUsageTracker( + process_id=input_data.process_id, + user_id=getattr(input_data, "user_id", "") or "", + ) + self.app_context._instances.pop(TokenUsageTracker, None) + self.app_context.add_singleton(TokenUsageTracker, token_tracker) + try: telemetry: TelemetryManager = await self.app_context.get_service_async( TelemetryManager @@ -714,6 +723,19 @@ async def _generate_report_summary( # print(f"{event.__class__.__name__} ({event.origin.value}): {event}") pass finally: + # Emit token usage summary events to Application Insights and persist to Cosmos + try: + token_tracker.emit_summary_events() + telemetry_mgr: TelemetryManager = ( + await self.app_context.get_service_async(TelemetryManager) + ) + await telemetry_mgr.persist_token_usage( + process_id=input_data.process_id, + token_summary=token_tracker.get_summary(), + ) + except Exception as e: + logger.warning("Failed to emit/persist token usage: %s", e) + # Clean up shared memory store if memory_store is not None: try: diff --git a/src/processor/src/utils/agent_telemetry.py b/src/processor/src/utils/agent_telemetry.py index 9e574377..15a0d188 100644 --- a/src/processor/src/utils/agent_telemetry.py +++ b/src/processor/src/utils/agent_telemetry.py @@ -296,6 +296,23 @@ class ProcessStatus(RootEntityBase["ProcessStatus", str]): description="Comprehensive UI data including file manifests, dashboard metrics, and downloadable artifacts", ) + # Token Usage Tracking + total_input_tokens: int = 0 + total_output_tokens: int = 0 + total_tokens: int = 0 + token_usage_by_agent: dict[str, dict[str, Any]] = Field( + default_factory=dict, + description="Token usage per agent: {agent_name: {input_tokens, output_tokens, total_tokens, call_count, model_deployment_name}}", + ) + token_usage_by_model: dict[str, dict[str, Any]] = Field( + default_factory=dict, + description="Token usage per model: {model_deployment_name: {input_tokens, output_tokens, total_tokens, call_count}}", + ) + token_usage_by_step: dict[str, dict[str, Any]] = Field( + default_factory=dict, + description="Token usage per step: {step_name: {input_tokens, output_tokens, total_tokens, call_count}}", + ) + class AgentActivityRepository(RepositoryBase[ProcessStatus, str]): def __init__(self, app_context: AppContext): @@ -1625,3 +1642,52 @@ async def get_ui_telemetry_data(self, process_id: str) -> dict[str, Any]: except Exception as e: logger.error(f"[UI-TELEMETRY] Failed to retrieve UI data: {e}") return {} + + async def persist_token_usage( + self, + process_id: str, + token_summary: dict[str, Any], + ) -> None: + """Persist aggregated token usage data to the ProcessStatus in Cosmos DB. + + Parameters + ---------- + process_id: + The process whose telemetry record should be updated. + token_summary: + The output of ``TokenUsageTracker.get_summary()``, containing + ``total``, ``by_agent``, ``by_model``, and ``by_step`` dictionaries. + """ + if not self.repository: + logger.info("[TELEMETRY] Development mode — token usage not persisted") + return + + try: + current_process = await self.repository.get_async(process_id) + if not current_process: + logger.warning( + "[TOKEN] Process %s not found — cannot persist token usage", + process_id, + ) + return + + total = token_summary.get("total", {}) + current_process.total_input_tokens = total.get("input_tokens", 0) + current_process.total_output_tokens = total.get("output_tokens", 0) + current_process.total_tokens = total.get("total_tokens", 0) + current_process.token_usage_by_agent = token_summary.get("by_agent", {}) + current_process.token_usage_by_model = token_summary.get("by_model", {}) + current_process.token_usage_by_step = token_summary.get("by_step", {}) + current_process.last_update_time = _get_utc_timestamp() + + await self.repository.update_async(current_process) + logger.info( + "[TOKEN] Persisted token usage for process %s: total=%d", + process_id, + current_process.total_tokens, + ) + except Exception: + logger.exception( + "[TOKEN] Failed to persist token usage (process_id=%s)", + process_id, + ) diff --git a/src/processor/src/utils/event_utils.py b/src/processor/src/utils/event_utils.py new file mode 100644 index 00000000..abbc1a89 --- /dev/null +++ b/src/processor/src/utils/event_utils.py @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Lightweight helpers for emitting Application Insights custom events from the processor service. + +Mirrors the backend-api ``track_event_if_configured`` pattern so the processor +can emit structured custom events (e.g. token usage) to the same Application +Insights workspace. +""" +from __future__ import annotations + +import logging +import os +from typing import Any, Mapping + +logger = logging.getLogger(__name__) + +APP_INSIGHTS_CONN_STRING_ENV = "APPLICATIONINSIGHTS_CONNECTION_STRING" + +_warned_unconfigured: bool = False + + +def _is_app_insights_configured() -> bool: + value = os.environ.get(APP_INSIGHTS_CONN_STRING_ENV) + return bool(value and value.strip()) + + +def track_event_if_configured( + name: str, properties: Mapping[str, Any] | None = None +) -> None: + """Emit an Application Insights custom event, gated on configuration. + + No-op when ``APPLICATIONINSIGHTS_CONNECTION_STRING`` is unset. + Swallows export failures so telemetry never breaks processing. + """ + global _warned_unconfigured + + if not _is_app_insights_configured(): + if not _warned_unconfigured: + logger.warning( + "APPLICATIONINSIGHTS_CONNECTION_STRING is not set; " + "track_event_if_configured(name=%s) is a no-op.", + name, + ) + _warned_unconfigured = True + return + + safe_properties: dict[str, Any] = dict(properties) if properties else {} + + try: + from azure.monitor.events.extension import track_event # type: ignore[import-not-found] + except ImportError: + logger.warning( + "azure-monitor-events-extension is not installed; " + "skipping track_event(name=%s).", + name, + ) + return + + try: + track_event(name, safe_properties) + except Exception: + logger.exception( + "Failed to publish App Insights custom event name=%s.", name + ) diff --git a/src/processor/src/utils/token_usage_tracker.py b/src/processor/src/utils/token_usage_tracker.py new file mode 100644 index 00000000..ae10a68d --- /dev/null +++ b/src/processor/src/utils/token_usage_tracker.py @@ -0,0 +1,403 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""LLM Token Usage Tracker for comprehensive consumption monitoring. + +Tracks token usage across four dimensions: +- Per agent (e.g. Chief_Architect, EKS_Expert) +- Per team/step (analysis, design, yaml, documentation) +- Per user/process +- Per model deployment + +Usage data is emitted to Application Insights as custom events and can be +persisted to Cosmos DB via the TelemetryManager. +""" +from __future__ import annotations + +import logging +import threading +from dataclasses import dataclass, field +from typing import Any + +from utils.event_utils import track_event_if_configured + +logger = logging.getLogger(__name__) + + +@dataclass +class TokenUsageRecord: + """Token counts for a single LLM interaction.""" + + input_tokens: int = 0 + output_tokens: int = 0 + total_tokens: int = 0 + + +@dataclass +class AggregatedTokenUsage: + """Accumulated token usage with call count.""" + + input_tokens: int = 0 + output_tokens: int = 0 + total_tokens: int = 0 + call_count: int = 0 + + def add(self, record: TokenUsageRecord) -> None: + self.input_tokens += record.input_tokens + self.output_tokens += record.output_tokens + self.total_tokens += record.total_tokens + self.call_count += 1 + + def to_dict(self) -> dict[str, int]: + return { + "input_tokens": self.input_tokens, + "output_tokens": self.output_tokens, + "total_tokens": self.total_tokens, + "call_count": self.call_count, + } + + +class TokenUsageTracker: + """Thread-safe tracker that aggregates LLM token usage across multiple dimensions. + + Accumulates usage per agent, per step (team), per model, and overall per process. + Emits Application Insights custom events for each recorded interaction and + provides summary emission at process completion. + """ + + def __init__(self, process_id: str, user_id: str = ""): + self.process_id = process_id + self.user_id = user_id + self._lock = threading.Lock() + + # Aggregation buckets + self._by_agent: dict[str, AggregatedTokenUsage] = {} + self._by_step: dict[str, AggregatedTokenUsage] = {} + self._by_model: dict[str, AggregatedTokenUsage] = {} + self._total = AggregatedTokenUsage() + + # Agent-to-model mapping for richer telemetry + self._agent_model_map: dict[str, str] = {} + + def set_agent_model(self, agent_name: str, model_deployment_name: str) -> None: + """Register the model deployment used by a specific agent.""" + with self._lock: + self._agent_model_map[agent_name] = model_deployment_name + + def record( + self, + *, + input_tokens: int, + output_tokens: int, + total_tokens: int, + agent_name: str = "", + step_name: str = "", + model_deployment_name: str = "", + ) -> None: + """Record a single LLM call's token usage. + + Accumulates into all relevant dimensions and emits a per-call + Application Insights event. + """ + if total_tokens <= 0 and input_tokens <= 0 and output_tokens <= 0: + return + + if total_tokens <= 0: + total_tokens = input_tokens + output_tokens + + record = TokenUsageRecord( + input_tokens=input_tokens, + output_tokens=output_tokens, + total_tokens=total_tokens, + ) + + # Resolve model from agent map if not provided + if not model_deployment_name and agent_name: + model_deployment_name = self._agent_model_map.get(agent_name, "") + + with self._lock: + self._total.add(record) + + if agent_name: + if agent_name not in self._by_agent: + self._by_agent[agent_name] = AggregatedTokenUsage() + self._by_agent[agent_name].add(record) + + if step_name: + if step_name not in self._by_step: + self._by_step[step_name] = AggregatedTokenUsage() + self._by_step[step_name].add(record) + + if model_deployment_name: + if model_deployment_name not in self._by_model: + self._by_model[model_deployment_name] = AggregatedTokenUsage() + self._by_model[model_deployment_name].add(record) + + # Emit per-call event to Application Insights + try: + track_event_if_configured( + "LLM_Token_Usage", + { + "process_id": self.process_id, + "user_id": self.user_id, + "agent_name": agent_name, + "step_name": step_name, + "model_deployment_name": model_deployment_name, + "input_tokens": str(input_tokens), + "output_tokens": str(output_tokens), + "total_tokens": str(total_tokens), + }, + ) + except Exception: + logger.debug("Failed to emit per-call token usage event", exc_info=True) + + logger.info( + "[TOKEN] Recorded: agent=%s step=%s model=%s input=%d output=%d total=%d | cumulative=%d", + agent_name, + step_name, + model_deployment_name, + input_tokens, + output_tokens, + total_tokens, + self._total.total_tokens, + ) + + def get_summary(self) -> dict[str, Any]: + """Return a snapshot of all accumulated token usage.""" + with self._lock: + return { + "process_id": self.process_id, + "user_id": self.user_id, + "total": self._total.to_dict(), + "by_agent": {k: v.to_dict() for k, v in self._by_agent.items()}, + "by_step": {k: v.to_dict() for k, v in self._by_step.items()}, + "by_model": {k: v.to_dict() for k, v in self._by_model.items()}, + } + + def emit_summary_events(self) -> None: + """Emit summary-level Application Insights custom events. + + Call this at the end of a process/workflow to produce aggregated events + that are easy to query in KQL. + """ + summary = self.get_summary() + + try: + # Overall summary + track_event_if_configured( + "LLM_Token_Usage_Summary", + { + "process_id": self.process_id, + "user_id": self.user_id, + "total_input_tokens": str(summary["total"]["input_tokens"]), + "total_output_tokens": str(summary["total"]["output_tokens"]), + "total_tokens": str(summary["total"]["total_tokens"]), + "total_calls": str(summary["total"]["call_count"]), + "agent_count": str(len(summary["by_agent"])), + "model_count": str(len(summary["by_model"])), + "step_count": str(len(summary["by_step"])), + }, + ) + + # Per-agent events + for agent_name, usage in summary["by_agent"].items(): + model = self._agent_model_map.get(agent_name, "") + track_event_if_configured( + "LLM_Agent_Token_Usage", + { + "process_id": self.process_id, + "user_id": self.user_id, + "agent_name": agent_name, + "model_deployment_name": model, + "input_tokens": str(usage["input_tokens"]), + "output_tokens": str(usage["output_tokens"]), + "total_tokens": str(usage["total_tokens"]), + "call_count": str(usage["call_count"]), + }, + ) + + # Per-model events + for model_name, usage in summary["by_model"].items(): + track_event_if_configured( + "LLM_Model_Token_Usage", + { + "process_id": self.process_id, + "user_id": self.user_id, + "model_deployment_name": model_name, + "input_tokens": str(usage["input_tokens"]), + "output_tokens": str(usage["output_tokens"]), + "total_tokens": str(usage["total_tokens"]), + "call_count": str(usage["call_count"]), + }, + ) + + # Per-step (team) events + for step_name, usage in summary["by_step"].items(): + track_event_if_configured( + "LLM_Step_Token_Usage", + { + "process_id": self.process_id, + "user_id": self.user_id, + "step_name": step_name, + "input_tokens": str(usage["input_tokens"]), + "output_tokens": str(usage["output_tokens"]), + "total_tokens": str(usage["total_tokens"]), + "call_count": str(usage["call_count"]), + }, + ) + + logger.info( + "[TOKEN] Emitted summary events: total=%d agents=%d models=%d steps=%d", + summary["total"]["total_tokens"], + len(summary["by_agent"]), + len(summary["by_model"]), + len(summary["by_step"]), + ) + except Exception: + logger.exception("[TOKEN] Failed to emit summary events") + + +def extract_usage_from_response(response: Any) -> TokenUsageRecord | None: + """Extract token usage from an agent_framework or OpenAI SDK response object. + + Handles multiple response shapes: + 1. response.usage (OpenAI SDK ChatCompletion) + 2. response.usage_details (agent_framework Content objects) + 3. response dict with usage keys + 4. AgentResponseUpdate with contents containing usage + """ + if response is None: + return None + + # 1. Direct .usage attribute (OpenAI ChatCompletion, Responses API) + usage = getattr(response, "usage", None) + if usage is not None: + record = _parse_usage_object(usage) + if record: + return record + + # 2. .usage_details or .details attribute + usage_details = getattr(response, "details", None) or getattr(response, "usage_details", None) + if usage_details is not None: + record = _parse_usage_object(usage_details) + if record: + return record + + # 3. raw_representation with usage + raw = getattr(response, "raw_representation", None) + if raw is not None: + raw_usage = getattr(raw, "usage", None) + if raw_usage is not None: + record = _parse_usage_object(raw_usage) + if record: + return record + if isinstance(raw, dict) and "usage" in raw: + record = _parse_usage_object(raw["usage"]) + if record: + return record + + # 4. contents list with usage items (AgentResponseUpdate) + contents = getattr(response, "contents", None) + if contents: + for item in contents: + item_type = getattr(item, "type", None) + if item_type == "usage": + # SDK UsageContent uses "details"; fall back to "usage_details" + ud = getattr(item, "details", None) or getattr(item, "usage_details", None) + if isinstance(ud, dict): + record = _parse_usage_object(ud) + if record: + return record + elif ud is not None: + record = _parse_usage_object(ud) + if record: + return record + # Direct details/usage_details on content item + ud = getattr(item, "details", None) or getattr(item, "usage_details", None) + if isinstance(ud, dict) and ud: + record = _parse_usage_object(ud) + if record: + return record + # Dict content item + if isinstance(item, dict): + if "details" in item: + record = _parse_usage_object(item["details"]) + if record: + return record + if "usage_details" in item: + record = _parse_usage_object(item["usage_details"]) + if record: + return record + if "input_token_count" in item or "total_token_count" in item: + record = _parse_usage_object(item) + if record: + return record + + # 5. additional_properties + addl = getattr(response, "additional_properties", None) + if isinstance(addl, dict) and "usage" in addl: + record = _parse_usage_object(addl["usage"]) + if record: + return record + + # 6. Dict response + if isinstance(response, dict): + if "usage" in response: + record = _parse_usage_object(response["usage"]) + if record: + return record + record = _parse_usage_object(response) + if record: + return record + + return None + + +def _parse_usage_object(usage: Any) -> TokenUsageRecord | None: + """Parse a usage object (dict or object with attrs) into a TokenUsageRecord.""" + if usage is None: + return None + + if isinstance(usage, dict): + inp = ( + usage.get("input_token_count", 0) + or usage.get("prompt_tokens", 0) + or usage.get("input_tokens", 0) + or 0 + ) + out = ( + usage.get("output_token_count", 0) + or usage.get("completion_tokens", 0) + or usage.get("output_tokens", 0) + or 0 + ) + tot = ( + usage.get("total_token_count", 0) + or usage.get("total_tokens", 0) + or (inp + out) + ) + else: + inp = ( + getattr(usage, "input_token_count", 0) + or getattr(usage, "prompt_tokens", 0) + or getattr(usage, "input_tokens", 0) + or 0 + ) + out = ( + getattr(usage, "output_token_count", 0) + or getattr(usage, "completion_tokens", 0) + or getattr(usage, "output_tokens", 0) + or 0 + ) + tot = ( + getattr(usage, "total_token_count", 0) + or getattr(usage, "total_tokens", 0) + or (inp + out) + ) + + if tot > 0 or inp > 0 or out > 0: + return TokenUsageRecord( + input_tokens=int(inp), + output_tokens=int(out), + total_tokens=int(tot) if tot > 0 else int(inp) + int(out), + ) + return None From a31f8f035338a366d9a23eb8162a96034830a41f Mon Sep 17 00:00:00 2001 From: Priyanka-Microsoft Date: Mon, 25 May 2026 15:27:11 +0530 Subject: [PATCH 08/50] optimize the code --- .../agent_framework/groupchat_orchestrator.py | 111 ++++++++---------- src/processor/src/utils/event_utils.py | 17 ++- .../src/utils/token_usage_tracker.py | 91 +++++--------- 3 files changed, 91 insertions(+), 128 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 50711fd3..1607b752 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -484,6 +484,7 @@ async def run_stream( self._tool_call_emitted.clear() self._tool_call_recorded.clear() self._tool_call_index.clear() + self._streaming_captured_usage = False self._conversation: list[ChatMessage] = [] # Track conversation during workflow try: @@ -686,70 +687,9 @@ async def _handle_agent_update( await self._process_tool_calls(event, agent_name, stream_callback) # Extract token usage from the streaming update if tracker is active. - # Check multiple paths where usage data may appear. if self.token_usage_tracker is not None: try: - data = event.data - # Path 1: data.contents with Content(type="usage") - contents = getattr(data, "contents", None) - if contents: - for item in contents: - ctype = getattr(item, "type", None) - if ctype == "usage": - # SDK UsageContent uses "details"; fall back to "usage_details" - ud = getattr(item, "details", None) or getattr(item, "usage_details", None) - if ud: - record = _parse_usage_object(ud) - if record and record.total_tokens > 0: - self.token_usage_tracker.record( - input_tokens=record.input_tokens, - output_tokens=record.output_tokens, - total_tokens=record.total_tokens, - agent_name=agent_name, - step_name=self.name, - ) - self._streaming_captured_usage = True - logger.info( - "[TOKEN_ORCH] recorded from contents: agent=%s step=%s tokens=%s", - agent_name, self.name, record.total_tokens, - ) - return - # Path 2: data.usage direct attribute - usage = getattr(data, "usage", None) - if usage is not None: - record = _parse_usage_object(usage) - if record and record.total_tokens > 0: - self.token_usage_tracker.record( - input_tokens=record.input_tokens, - output_tokens=record.output_tokens, - total_tokens=record.total_tokens, - agent_name=agent_name, - step_name=self.name, - ) - self._streaming_captured_usage = True - logger.info( - "[TOKEN_ORCH] recorded from usage attr: agent=%s step=%s tokens=%s", - agent_name, self.name, record.total_tokens, - ) - return - # Path 3: event itself may carry usage - event_usage = getattr(event, "usage", None) - if event_usage is not None: - record = _parse_usage_object(event_usage) - if record and record.total_tokens > 0: - self.token_usage_tracker.record( - input_tokens=record.input_tokens, - output_tokens=record.output_tokens, - total_tokens=record.total_tokens, - agent_name=agent_name, - step_name=self.name, - ) - self._streaming_captured_usage = True - logger.info( - "[TOKEN_ORCH] recorded from event.usage: agent=%s step=%s tokens=%s", - agent_name, self.name, record.total_tokens, - ) - return + self._try_record_streaming_usage(event, agent_name) except Exception: logger.debug( "Failed to extract token usage from update (agent=%s)", @@ -757,6 +697,53 @@ async def _handle_agent_update( exc_info=True, ) + def _try_record_streaming_usage(self, event: Any, agent_name: str) -> None: + """Try to extract and record token usage from a streaming event. + + Checks three paths in priority order: + 1. event.data.contents with Content(type="usage") + 2. event.data.usage direct attribute + 3. event.usage top-level attribute + """ + candidates: list[tuple[Any, str]] = [] + data = event.data + + # Path 1: data.contents with Content(type="usage") + contents = getattr(data, "contents", None) + if contents: + for item in contents: + if getattr(item, "type", None) == "usage": + ud = getattr(item, "details", None) or getattr(item, "usage_details", None) + if ud: + candidates.append((ud, "contents")) + + # Path 2: data.usage + usage = getattr(data, "usage", None) + if usage is not None: + candidates.append((usage, "data.usage")) + + # Path 3: event.usage + event_usage = getattr(event, "usage", None) + if event_usage is not None: + candidates.append((event_usage, "event.usage")) + + for candidate, source in candidates: + record = _parse_usage_object(candidate) + if record and record.total_tokens > 0: + self.token_usage_tracker.record( + input_tokens=record.input_tokens, + output_tokens=record.output_tokens, + total_tokens=record.total_tokens, + agent_name=agent_name, + step_name=self.name, + ) + self._streaming_captured_usage = True + logger.info( + "[TOKEN_ORCH] recorded from %s: agent=%s step=%s tokens=%s", + source, agent_name, self.name, record.total_tokens, + ) + return + def _normalize_executor_id(self, executor_id: str) -> str: """Normalize executor id to agent name. diff --git a/src/processor/src/utils/event_utils.py b/src/processor/src/utils/event_utils.py index abbc1a89..dc43e8fd 100644 --- a/src/processor/src/utils/event_utils.py +++ b/src/processor/src/utils/event_utils.py @@ -16,6 +16,11 @@ APP_INSIGHTS_CONN_STRING_ENV = "APPLICATIONINSIGHTS_CONNECTION_STRING" +_UNCONFIGURED_WARNING = ( + "APPLICATIONINSIGHTS_CONNECTION_STRING is not set; " + "track_event_if_configured(name=%s) is a no-op." +) + _warned_unconfigured: bool = False @@ -24,6 +29,12 @@ def _is_app_insights_configured() -> bool: return bool(value and value.strip()) +def reset_unconfigured_warning_for_tests() -> None: + """Test-only helper: reset the once-per-process warning latch.""" + global _warned_unconfigured + _warned_unconfigured = False + + def track_event_if_configured( name: str, properties: Mapping[str, Any] | None = None ) -> None: @@ -36,11 +47,7 @@ def track_event_if_configured( if not _is_app_insights_configured(): if not _warned_unconfigured: - logger.warning( - "APPLICATIONINSIGHTS_CONNECTION_STRING is not set; " - "track_event_if_configured(name=%s) is a no-op.", - name, - ) + logger.warning(_UNCONFIGURED_WARNING, name) _warned_unconfigured = True return diff --git a/src/processor/src/utils/token_usage_tracker.py b/src/processor/src/utils/token_usage_tracker.py index ae10a68d..25e5e0b6 100644 --- a/src/processor/src/utils/token_usage_tracker.py +++ b/src/processor/src/utils/token_usage_tracker.py @@ -299,34 +299,23 @@ def extract_usage_from_response(response: Any) -> TokenUsageRecord | None: contents = getattr(response, "contents", None) if contents: for item in contents: - item_type = getattr(item, "type", None) - if item_type == "usage": - # SDK UsageContent uses "details"; fall back to "usage_details" + # Try usage-typed content items first, then any item with details + ud = None + if getattr(item, "type", None) == "usage": ud = getattr(item, "details", None) or getattr(item, "usage_details", None) - if isinstance(ud, dict): - record = _parse_usage_object(ud) - if record: - return record - elif ud is not None: - record = _parse_usage_object(ud) - if record: - return record - # Direct details/usage_details on content item - ud = getattr(item, "details", None) or getattr(item, "usage_details", None) - if isinstance(ud, dict) and ud: + if ud is None: + ud = getattr(item, "details", None) or getattr(item, "usage_details", None) + if ud is not None: record = _parse_usage_object(ud) if record: return record # Dict content item if isinstance(item, dict): - if "details" in item: - record = _parse_usage_object(item["details"]) - if record: - return record - if "usage_details" in item: - record = _parse_usage_object(item["usage_details"]) - if record: - return record + for key in ("details", "usage_details"): + if key in item: + record = _parse_usage_object(item[key]) + if record: + return record if "input_token_count" in item or "total_token_count" in item: record = _parse_usage_object(item) if record: @@ -352,52 +341,32 @@ def extract_usage_from_response(response: Any) -> TokenUsageRecord | None: return None +def _get_field(obj: Any, *names: str) -> int: + """Read the first non-zero value from *obj* for the given field names. + + Works uniformly for dicts (via ``get``) and objects (via ``getattr``). + """ + getter = obj.get if isinstance(obj, dict) else lambda k, d=0: getattr(obj, k, d) + for name in names: + val = getter(name, 0) + if val: + return int(val) + return 0 + + def _parse_usage_object(usage: Any) -> TokenUsageRecord | None: """Parse a usage object (dict or object with attrs) into a TokenUsageRecord.""" if usage is None: return None - if isinstance(usage, dict): - inp = ( - usage.get("input_token_count", 0) - or usage.get("prompt_tokens", 0) - or usage.get("input_tokens", 0) - or 0 - ) - out = ( - usage.get("output_token_count", 0) - or usage.get("completion_tokens", 0) - or usage.get("output_tokens", 0) - or 0 - ) - tot = ( - usage.get("total_token_count", 0) - or usage.get("total_tokens", 0) - or (inp + out) - ) - else: - inp = ( - getattr(usage, "input_token_count", 0) - or getattr(usage, "prompt_tokens", 0) - or getattr(usage, "input_tokens", 0) - or 0 - ) - out = ( - getattr(usage, "output_token_count", 0) - or getattr(usage, "completion_tokens", 0) - or getattr(usage, "output_tokens", 0) - or 0 - ) - tot = ( - getattr(usage, "total_token_count", 0) - or getattr(usage, "total_tokens", 0) - or (inp + out) - ) + inp = _get_field(usage, "input_token_count", "prompt_tokens", "input_tokens") + out = _get_field(usage, "output_token_count", "completion_tokens", "output_tokens") + tot = _get_field(usage, "total_token_count", "total_tokens") or (inp + out) if tot > 0 or inp > 0 or out > 0: return TokenUsageRecord( - input_tokens=int(inp), - output_tokens=int(out), - total_tokens=int(tot) if tot > 0 else int(inp) + int(out), + input_tokens=inp, + output_tokens=out, + total_tokens=tot if tot > 0 else inp + out, ) return None From f11248b55a3ff81ceb66f735148688dbb7061b83 Mon Sep 17 00:00:00 2001 From: Priyanka-Microsoft Date: Wed, 27 May 2026 17:45:42 +0530 Subject: [PATCH 09/50] optimize the code --- .../app/libs/logging/llm_token_telemetry.py | 935 ++++++++++++++++++ .../src/tests/test_llm_token_telemetry.py | 572 +++++++++++ .../src/utils/llm_token_telemetry.py | 935 ++++++++++++++++++ .../src/utils/token_usage_tracker.py | 287 ++---- 4 files changed, 2548 insertions(+), 181 deletions(-) create mode 100644 src/backend-api/src/app/libs/logging/llm_token_telemetry.py create mode 100644 src/processor/src/tests/test_llm_token_telemetry.py create mode 100644 src/processor/src/utils/llm_token_telemetry.py diff --git a/src/backend-api/src/app/libs/logging/llm_token_telemetry.py b/src/backend-api/src/app/libs/logging/llm_token_telemetry.py new file mode 100644 index 00000000..b3035fc8 --- /dev/null +++ b/src/backend-api/src/app/libs/logging/llm_token_telemetry.py @@ -0,0 +1,935 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Cross-accelerator LLM token-usage telemetry helpers. + +A single, dependency-light helper module that can be dropped into any Microsoft +Solution Accelerator to capture LLM token usage and emit standardized custom +events to Application Insights. + +Why this file exists +-------------------- +Seven solution accelerators have independently shipped near-identical +``token_usage_utils.py`` modules (see PRs: content-generation #860, CKM #933, +content-processing #586, Container-Migration #257, agentic-data-foundation +#383, customer-chatbot #218, MACAE #1003). They all: + +* extract token counts from agent_framework / Azure OpenAI responses, +* emit the same three custom events (``LLM_Token_Usage_Summary``, + ``LLM_Agent_Token_Usage``, ``LLM_Model_Token_Usage``), +* defensively swallow telemetry errors, +* duplicate the same KQL queries and Azure Workbook. + +This module consolidates the union of those behaviours behind one stable API +so each accelerator can replace its bespoke helper with an import. + +Public API +---------- +- ``TokenUsage`` -- immutable dataclass for counts +- ``extract_usage(obj)`` -- agent_framework run result / message +- ``extract_usage_from_dict(d)`` -- raw dict from any SDK +- ``extract_usage_from_stream_chunk`` -- streaming chunks +- ``extract_realtime_usage(resp)`` -- Azure AI Voice Live response.done +- ``TokenUsageEmitter`` -- emits the three events + optional + per-user / per-team / speech events +- ``TokenUsageScope`` -- context-manager that accumulates and + auto-emits on exit +- ``track_tokens`` -- decorator wrapper around the scope + +Design rules +------------ +* Telemetry NEVER raises. Extraction failures return ``None``; emission + failures are logged at WARNING. +* No hard dependency on ``azure-monitor-events-extension``; if absent the + emitter degrades to logging only. +* Arbitrary correlation dimensions are passed as ``**dimensions`` kwargs and + surface verbatim as custom-event properties. This is how each accelerator + attaches its own keys (``conversation_id``, ``process_id``, ``team_name``, + ``file_name``, ``tenant``, etc.) without forking the helper. +""" +from __future__ import annotations + +import asyncio +import functools +import logging +import os +import random +from contextlib import AbstractContextManager +from dataclasses import dataclass, field +from typing import Any, Callable, Iterable, Mapping, Optional +from unittest.mock import NonCallableMock + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Event-name constants -- keep these stable; KQL queries and workbooks bind +# to these exact strings. +# --------------------------------------------------------------------------- +EVENT_SUMMARY = "LLM_Token_Usage_Summary" +EVENT_AGENT = "LLM_Agent_Token_Usage" +EVENT_MODEL = "LLM_Model_Token_Usage" +EVENT_USER = "LLM_User_Token_Usage" +EVENT_TEAM = "LLM_Team_Token_Usage" +EVENT_SPEECH = "Speech_Usage" + + +# Token-count field aliases observed across model providers / SDK versions. +_INPUT_KEYS = ( + "input_token_count", + "input_tokens", + "prompt_tokens", + "promptTokens", +) +_OUTPUT_KEYS = ( + "output_token_count", + "output_tokens", + "completion_tokens", + "completionTokens", +) +_TOTAL_KEYS = ( + "total_token_count", + "total_tokens", + "totalTokens", +) + + +# --------------------------------------------------------------------------- +# Data model +# --------------------------------------------------------------------------- +@dataclass(frozen=True) +class TokenUsage: + """Normalized token-usage record.""" + + input_tokens: int = 0 + output_tokens: int = 0 + total_tokens: int = 0 + + # Optional realtime / voice fields (None unless populated) + input_audio_tokens: Optional[int] = None + input_text_tokens: Optional[int] = None + input_cached_tokens: Optional[int] = None + output_audio_tokens: Optional[int] = None + output_text_tokens: Optional[int] = None + + @property + def has_any(self) -> bool: + return bool(self.input_tokens or self.output_tokens or self.total_tokens) + + def __add__(self, other: "TokenUsage") -> "TokenUsage": + if not isinstance(other, TokenUsage): + return NotImplemented + + def _sum(a: Optional[int], b: Optional[int]) -> Optional[int]: + if a is None and b is None: + return None + return (a or 0) + (b or 0) + + return TokenUsage( + input_tokens=self.input_tokens + other.input_tokens, + output_tokens=self.output_tokens + other.output_tokens, + total_tokens=self.total_tokens + other.total_tokens, + input_audio_tokens=_sum(self.input_audio_tokens, other.input_audio_tokens), + input_text_tokens=_sum(self.input_text_tokens, other.input_text_tokens), + input_cached_tokens=_sum(self.input_cached_tokens, other.input_cached_tokens), + output_audio_tokens=_sum(self.output_audio_tokens, other.output_audio_tokens), + output_text_tokens=_sum(self.output_text_tokens, other.output_text_tokens), + ) + + def to_event_props(self) -> dict[str, str]: + """Stringified property bag suitable for App Insights custom events.""" + props: dict[str, str] = { + "input_tokens": str(self.input_tokens), + "output_tokens": str(self.output_tokens), + "total_tokens": str(self.total_tokens), + } + for name in ( + "input_audio_tokens", + "input_text_tokens", + "input_cached_tokens", + "output_audio_tokens", + "output_text_tokens", + ): + value = getattr(self, name) + if value is not None: + props[name] = str(value) + return props + + +# --------------------------------------------------------------------------- +# Low-level coercion helpers +# --------------------------------------------------------------------------- +def _to_int(value: Any, default: int = 0) -> int: + """Best-effort int conversion; bool excluded; never raises.""" + if value is None or isinstance(value, bool): + return default + if isinstance(value, int): + return value + if isinstance(value, float): + return int(value) + if isinstance(value, str): + s = value.strip() + if s.isdigit(): + return int(s) + try: + return int(value) + except (TypeError, ValueError): + return default + + +def _get(obj: Any, key: str, default: Any = None) -> Any: + """Read an attribute or dict key uniformly.""" + if obj is None: + return default + if isinstance(obj, Mapping): + return obj.get(key, default) + return getattr(obj, key, default) + + +def _is_iterable(obj: Any) -> bool: + """True only for real iterables (lists/tuples/sets/generators), NOT for + arbitrary objects (e.g. ``unittest.mock.Mock``) that happen to expose + ``__iter__`` but blow up on iteration.""" + if obj is None: + return False + if isinstance(obj, (list, tuple, set, frozenset)): + return True + # Strings are iterable but never the right answer for "messages". + if isinstance(obj, (str, bytes, bytearray, Mapping)): + return False + # Fall back to a duck-typed check, but reject Mock instances which would + # otherwise pretend to support iteration. + if isinstance(obj, NonCallableMock): + return False + return hasattr(obj, "__iter__") + + +def _read_counts(usage_obj: Any) -> Optional[TokenUsage]: + """Read ``input/output/total`` from any usage-bearing object/dict.""" + if usage_obj is None: + return None + + inp = out = tot = 0 + for k in _INPUT_KEYS: + v = _get(usage_obj, k) + if v: + inp = _to_int(v) + break + for k in _OUTPUT_KEYS: + v = _get(usage_obj, k) + if v: + out = _to_int(v) + break + for k in _TOTAL_KEYS: + v = _get(usage_obj, k) + if v: + tot = _to_int(v) + break + + if tot == 0 and (inp or out): + tot = inp + out + if not (inp or out or tot): + return None + return TokenUsage(input_tokens=inp, output_tokens=out, total_tokens=tot) + + +# --------------------------------------------------------------------------- +# Extraction -- public +# --------------------------------------------------------------------------- +def extract_usage(result: Any) -> Optional[TokenUsage]: + """Extract usage from an agent_framework run result, ChatMessage, or + OpenAI-style ChatCompletion. + + Checks (in order): + 1. ``result.usage_details`` or ``result.usage`` + 2. ``result.raw_representation.usage`` (OpenAI ChatCompletion shape) + 3. Aggregated ``result.messages[*].contents[*].usage_details`` + + Never raises -- returns ``None`` on any unexpected shape. + """ + if result is None: + return None + + try: + for attr in ("usage_details", "usage"): + found = _read_counts(_get(result, attr)) + if found: + return found + + raw = _get(result, "raw_representation") + if raw is not None: + found = _read_counts(_get(raw, "usage")) + if found: + return found + + aggregated = TokenUsage() + found_any = False + messages = _get(result, "messages") + if not _is_iterable(messages): + return None + for msg in messages: + contents = _get(msg, "contents") + if not _is_iterable(contents): + continue + for content in contents: + usage = _get(content, "usage_details") or _get(content, "usage") + piece = _read_counts(usage) + if piece: + aggregated = aggregated + piece + found_any = True + return aggregated if found_any else None + except Exception as exc: + logger.debug("extract_usage failed: %s", exc, exc_info=True) + return None + + +def extract_usage_from_dict(data: Any) -> Optional[TokenUsage]: + """Extract from a raw dict / SDK usage object.""" + return _read_counts(data) + + +def extract_usage_from_stream_chunk(chunk: Any) -> Optional[TokenUsage]: + """Streaming chunks: try the top-level shape, then ``chunk.metadata.usage``.""" + found = extract_usage(chunk) + if found: + return found + metadata = _get(chunk, "metadata") + if metadata is not None: + return _read_counts(_get(metadata, "usage")) + return None + + +def extract_realtime_usage(response_obj: Any) -> Optional[TokenUsage]: + """Azure AI Voice Live ``response.done`` payload extractor. + + Includes audio / text / cached sub-counts when present. + """ + usage = _get(response_obj, "usage") + if usage is None: + return None + + inp = _to_int(_get(usage, "input_tokens")) + out = _to_int(_get(usage, "output_tokens")) + tot = _to_int(_get(usage, "total_tokens")) + if tot == 0 and (inp or out): + tot = inp + out + + in_details = _get(usage, "input_token_details") or {} + out_details = _get(usage, "output_token_details") or {} + + record = TokenUsage( + input_tokens=inp, + output_tokens=out, + total_tokens=tot, + input_audio_tokens=_to_int(_get(in_details, "audio_tokens")), + input_text_tokens=_to_int(_get(in_details, "text_tokens")), + input_cached_tokens=_to_int(_get(in_details, "cached_tokens")), + output_audio_tokens=_to_int(_get(out_details, "audio_tokens")), + output_text_tokens=_to_int(_get(out_details, "text_tokens")), + ) + # Only return if at least one non-zero count surfaced. + if record.has_any or any( + v for v in ( + record.input_audio_tokens, + record.input_text_tokens, + record.input_cached_tokens, + record.output_audio_tokens, + record.output_text_tokens, + ) + ): + return record + return None + + +# --------------------------------------------------------------------------- +# Tool / sub-agent attribution +# --------------------------------------------------------------------------- +def detect_invoked_tools(result: Any) -> set[str]: + """Return the set of tool/function names invoked in an agent result, + inferred from ``function_call`` content items. + + Used by orchestrators that expose sub-agents via ``.as_tool()`` to attribute + token usage only to the sub-agents that were actually called. Never raises. + """ + invoked: set[str] = set() + try: + messages = _get(result, "messages") + if not _is_iterable(messages): + return invoked + for msg in messages: + contents = _get(msg, "contents") + if not _is_iterable(contents): + continue + for content in contents: + if _get(content, "type") == "function_call": + name = _get(content, "name") + if name: + invoked.add(str(name)) + except Exception as exc: + logger.debug("detect_invoked_tools failed: %s", exc, exc_info=True) + return invoked + + +# --------------------------------------------------------------------------- +# Event sink (optional Application Insights dependency) +# --------------------------------------------------------------------------- +EventSink = Callable[[str, Mapping[str, str]], None] + + +def _default_event_sink() -> Optional[EventSink]: + """Return ``azure.monitor.events.extension.track_event`` if importable, + else ``None``. Resolved lazily so the helper still works in unit tests + without the dependency installed.""" + try: + from azure.monitor.events.extension import track_event # type: ignore + except Exception: # pragma: no cover - optional dep + return None + return track_event + + +# --------------------------------------------------------------------------- +# Emitter +# --------------------------------------------------------------------------- +class TokenUsageEmitter: + """Emit standardized token-usage custom events. + + Parameters + ---------- + connection_string: + Application Insights connection string. If ``None`` (default), the + ``APPLICATIONINSIGHTS_CONNECTION_STRING`` env var is consulted. When + no connection string is configured the emitter logs and skips the + ``track_event`` call. + static_dimensions: + Properties merged into every event (e.g. ``{"app": "customer-chatbot"}``). + event_sink: + Callable ``(event_name, props_dict) -> None``. Defaults to + ``azure.monitor.events.extension.track_event``. Override in tests. + pricing: + Optional mapping ``{model_deployment_name -> (usd_per_1k_input, + usd_per_1k_output)}``. When provided, an ``estimated_cost_usd`` + property is attached to agent / model / summary events. Model lookup + is case-insensitive. Use this to avoid hard-coding rates in KQL. + user_id_hasher: + Optional callable ``str -> str`` applied to any ``user_id`` value + before it leaves the emitter. Use this to satisfy PII / GDPR + requirements (e.g. HMAC-SHA256 with a tenant-scoped salt). Applied + to both ``static_dimensions['user_id']`` (at construction) and + per-call ``user_id`` kwargs. + sample_rate: + Fraction of high-cardinality events (agent / model / user / team / + speech) actually shipped, in ``[0.0, 1.0]``. The cheap **summary + event always fires** regardless of sample_rate so per-request totals + remain accurate; only the per-dimension breakdown is sampled. + Defaults to ``1.0`` (no sampling). + logger: + Override the module logger. + """ + + def __init__( + self, + *, + connection_string: Optional[str] = None, + static_dimensions: Optional[Mapping[str, Any]] = None, + event_sink: Optional[EventSink] = None, + pricing: Optional[Mapping[str, tuple[float, float]]] = None, + user_id_hasher: Optional[Callable[[str], str]] = None, + sample_rate: float = 1.0, + logger: Optional[logging.Logger] = None, + ) -> None: + self._cs = connection_string if connection_string is not None else os.getenv( + "APPLICATIONINSIGHTS_CONNECTION_STRING" + ) + self._sink = event_sink if event_sink is not None else _default_event_sink() + self._log = logger or logging.getLogger(__name__) + + # PII hashing applied to user_id everywhere. + self._user_id_hasher = user_id_hasher + + # Sampling clamp to [0, 1]. + try: + sr = float(sample_rate) + except (TypeError, ValueError): + sr = 1.0 + self._sample_rate = max(0.0, min(1.0, sr)) + + # Case-insensitive pricing lookup. Values stored as a (in, out) tuple. + self._pricing: dict[str, tuple[float, float]] = {} + for model, rates in (pricing or {}).items(): + if not model or rates is None: + continue + try: + inp, out = rates + self._pricing[str(model).lower()] = (float(inp), float(out)) + except (TypeError, ValueError): + self._log.warning("Ignoring malformed pricing entry: %s=%r", model, rates) + + # Pre-stringify static dims once. user_id (if present) is hashed here + # so the raw value is never retained on the emitter. + raw_static = dict(static_dimensions or {}) + if "user_id" in raw_static: + raw_static["user_id"] = self._apply_user_id_hash(raw_static["user_id"]) + self._static: dict[str, str] = { + k: ("" if v is None else str(v)) for k, v in raw_static.items() + } + + # -- public surface --------------------------------------------------- + @property + def enabled(self) -> bool: + return bool(self._cs) and self._sink is not None + + @property + def sample_rate(self) -> float: + return self._sample_rate + + # -- internal helpers ------------------------------------------------- + def _apply_user_id_hash(self, value: Any) -> Any: + """Apply the configured user_id_hasher; never raises.""" + if value is None or value == "" or self._user_id_hasher is None: + return value + try: + return self._user_id_hasher(str(value)) + except Exception as exc: # never let hashing break telemetry + self._log.warning("user_id_hasher raised: %s", exc) + return value + + def _should_sample(self) -> bool: + """Sampling decision for high-cardinality events.""" + if self._sample_rate >= 1.0: + return True + if self._sample_rate <= 0.0: + return False + return random.random() < self._sample_rate + + def _cost_props( + self, model_deployment_name: Optional[str], usage: TokenUsage + ) -> dict[str, str]: + """Return ``{'estimated_cost_usd': '...'}`` when pricing is configured + for the given model, else ``{}``. 6-decimal formatting.""" + if not self._pricing or not model_deployment_name: + return {} + rate = self._pricing.get(model_deployment_name.lower()) + if not rate: + return {} + inp_rate, out_rate = rate + cost = (usage.input_tokens * inp_rate + usage.output_tokens * out_rate) / 1000.0 + return {"estimated_cost_usd": f"{cost:.6f}"} + + def _summary_cost_props( + self, + primary_model: Optional[str], + additional_agents: Mapping[str, str], + usage: TokenUsage, + ) -> dict[str, str]: + """Best-effort cost for the summary event: charge full usage at the + primary model's rate (the SDK aggregates sub-agent tokens to the + orchestrator, so apportioning is not possible without per-agent + usage). Falls back to silent skip when no rate is known.""" + if primary_model: + cost = self._cost_props(primary_model, usage) + if cost: + return cost + for m in additional_agents.values(): + cost = self._cost_props(m, usage) + if cost: + return cost + return {} + + def emit(self, event_name: str, **dimensions: Any) -> None: + """Low-level: emit an event with arbitrary properties. + + Non-string values are stringified. ``None`` values are dropped. Any + ``user_id`` value is passed through the configured hasher. + Never raises. + """ + props = dict(self._static) # cheap shallow copy of pre-stringified dims + for k, v in dimensions.items(): + if v is None: + continue + if k == "user_id": + v = self._apply_user_id_hash(v) + if v is None or v == "": + continue + props[k] = v if isinstance(v, str) else str(v) + + if not self.enabled: + self._log.debug( + "App Insights not configured -- skipping event %s (%s)", + event_name, props, + ) + return + try: + self._sink(event_name, props) # type: ignore[misc] + except Exception as exc: # never break the caller + self._log.warning("track_event(%s) failed: %s", event_name, exc) + + # -- typed convenience emitters -------------------------------------- + def emit_agent( + self, + *, + agent_name: str, + model_deployment_name: str, + usage: TokenUsage, + **dimensions: Any, + ) -> None: + if not usage.has_any or not self._should_sample(): + return + self.emit( + EVENT_AGENT, + agent_name=agent_name, + model_deployment_name=model_deployment_name, + **usage.to_event_props(), + **self._cost_props(model_deployment_name, usage), + **dimensions, + ) + + def emit_model( + self, + *, + model_deployment_name: str, + usage: TokenUsage, + **dimensions: Any, + ) -> None: + if not usage.has_any or not self._should_sample(): + return + self.emit( + EVENT_MODEL, + model_deployment_name=model_deployment_name, + **usage.to_event_props(), + **self._cost_props(model_deployment_name, usage), + **dimensions, + ) + + def emit_user( + self, + *, + user_id: str, + usage: TokenUsage, + **dimensions: Any, + ) -> None: + if not usage.has_any or not user_id or not self._should_sample(): + return + self.emit( + EVENT_USER, + user_id=user_id, + **usage.to_event_props(), + **dimensions, + ) + + def emit_team( + self, + *, + team_name: str, + usage: TokenUsage, + **dimensions: Any, + ) -> None: + if not usage.has_any or not team_name or not self._should_sample(): + return + self.emit( + EVENT_TEAM, + team_name=team_name, + **usage.to_event_props(), + **dimensions, + ) + + def emit_summary( + self, + *, + usage: TokenUsage, + agent_count: int = 1, + model_count: int = 1, + primary_model: Optional[str] = None, + additional_agents: Optional[Mapping[str, str]] = None, + **dimensions: Any, + ) -> None: + """The summary event always fires (ignores ``sample_rate``) so per- + request totals remain accurate even when high-cardinality events are + sampled.""" + if not usage.has_any: + return + # Summary historically uses ``total_input_tokens`` / ``total_output_tokens`` + # field names; preserve that wire format for backward compatibility. + props = { + "total_input_tokens": str(usage.input_tokens), + "total_output_tokens": str(usage.output_tokens), + "total_tokens": str(usage.total_tokens), + "agent_count": str(agent_count), + "model_count": str(model_count), + "sample_rate": f"{self._sample_rate:.4f}", + } + # Carry over realtime sub-counts if present. + for k, v in usage.to_event_props().items(): + props.setdefault(k, v) + # Optional total cost. + props.update(self._summary_cost_props(primary_model, additional_agents or {}, usage)) + self.emit(EVENT_SUMMARY, **props, **dimensions) + + def emit_speech( + self, + *, + model_deployment_name: str, + source: str, + usage: TokenUsage, + **dimensions: Any, + ) -> None: + """Voice-Live / realtime speech usage event.""" + if not self._should_sample(): + return + self.emit( + EVENT_SPEECH, + model_deployment_name=model_deployment_name, + source=source, + **usage.to_event_props(), + **self._cost_props(model_deployment_name, usage), + **dimensions, + ) + + # -- combined emit: summary + agent + per-distinct-model --------------- + def emit_all( + self, + *, + agent_name: str, + model_deployment_name: str, + usage: TokenUsage, + additional_agents: Optional[Mapping[str, str]] = None, + emit_user_event: bool = False, + emit_team_event: bool = False, + **dimensions: Any, + ) -> None: + """Convenience: emit summary, agent, and one model event per distinct + model deployment in one shot. + + ``additional_agents`` maps sub-agent name -> its model deployment name + so callers can describe orchestrators that involve multiple agents. + + ``emit_user_event`` / ``emit_team_event`` opt in to the user/team + events; ``user_id`` / ``team_name`` must be present in dimensions for + those to fire. + """ + if not usage.has_any: + return + + agents = {agent_name: model_deployment_name} + if additional_agents: + agents.update({k: v for k, v in additional_agents.items() if k}) + models = {m for m in agents.values() if m} + + self.emit_summary( + usage=usage, + agent_count=len(agents), + model_count=len(models) or 1, + primary_model=model_deployment_name, + additional_agents=additional_agents, + **dimensions, + ) + self.emit_agent( + agent_name=agent_name, + model_deployment_name=model_deployment_name, + usage=usage, + **dimensions, + ) + for model in models: + self.emit_model( + model_deployment_name=model, + usage=usage, + **dimensions, + ) + if emit_user_event and dimensions.get("user_id"): + self.emit_user( + user_id=str(dimensions["user_id"]), + usage=usage, + agent_name=agent_name, + model_deployment_name=model_deployment_name, + ) + if emit_team_event and dimensions.get("team_name"): + self.emit_team( + team_name=str(dimensions["team_name"]), + usage=usage, + agent_name=agent_name, + model_deployment_name=model_deployment_name, + ) + + self._log.info( + "[TOKEN USAGE] agent=%s model=%s input=%d output=%d total=%d %s", + agent_name, + model_deployment_name, + usage.input_tokens, + usage.output_tokens, + usage.total_tokens, + " ".join(f"{k}={v}" for k, v in dimensions.items() if v), + ) + + +# --------------------------------------------------------------------------- +# Scope / decorator sugar +# --------------------------------------------------------------------------- +@dataclass +class TokenUsageScope(AbstractContextManager): + """Accumulate usage across multiple results, then emit on exit. + + Example:: + + with TokenUsageScope(emitter, + agent_name="chat", + model_deployment_name=cfg.model, + user_id=user_id) as scope: + result = await agent.run(prompt) + scope.add(result) # extracts and accumulates + """ + + emitter: TokenUsageEmitter + agent_name: str + model_deployment_name: str + dimensions: dict[str, Any] = field(default_factory=dict) + additional_agents: dict[str, str] = field(default_factory=dict) + emit_user_event: bool = False + emit_team_event: bool = False + usage: TokenUsage = field(default_factory=TokenUsage) + + def __init__( + self, + emitter: TokenUsageEmitter, + *, + agent_name: str, + model_deployment_name: str, + additional_agents: Optional[Mapping[str, str]] = None, + emit_user_event: bool = False, + emit_team_event: bool = False, + **dimensions: Any, + ) -> None: + self.emitter = emitter + self.agent_name = agent_name + self.model_deployment_name = model_deployment_name + self.additional_agents = dict(additional_agents or {}) + self.emit_user_event = emit_user_event + self.emit_team_event = emit_team_event + self.dimensions = dict(dimensions) + self.usage = TokenUsage() + + # -- accumulation ----------------------------------------------------- + def add(self, source: Any) -> Optional[TokenUsage]: + """Extract usage from any supported shape and add to the running total. + + Never raises -- extraction failures return ``None`` and are logged + at DEBUG. + """ + try: + found = extract_usage(source) or extract_usage_from_stream_chunk(source) + except Exception as exc: # belt + braces; extractors are already safe + logger.debug("TokenUsageScope.add failed: %s", exc, exc_info=True) + return None + if found: + self.usage = self.usage + found + return found + + def add_usage(self, usage: TokenUsage) -> None: + self.usage = self.usage + usage + + def add_chunks(self, chunks: Iterable[Any]) -> None: + for c in chunks: + self.add(c) + + # -- context manager -------------------------------------------------- + def __exit__(self, exc_type, exc, tb) -> None: + # Always emit (best-effort) regardless of exception status. + try: + self.emitter.emit_all( + agent_name=self.agent_name, + model_deployment_name=self.model_deployment_name, + usage=self.usage, + additional_agents=self.additional_agents, + emit_user_event=self.emit_user_event, + emit_team_event=self.emit_team_event, + **self.dimensions, + ) + except Exception as emit_exc: # pragma: no cover - belt + braces + logger.warning("TokenUsageScope emit failed: %s", emit_exc) + return None # do not suppress exceptions + + +def track_tokens( + emitter: TokenUsageEmitter, + *, + agent_name: str, + model_deployment_name: str, + dimension_args: Optional[Mapping[str, str]] = None, + additional_agents: Optional[Mapping[str, str]] = None, + emit_user_event: bool = False, + emit_team_event: bool = False, +): + """Decorator: wrap an async or sync function that returns an LLM result. + + ``dimension_args`` maps emitted-property-name -> callable-keyword-argument + name so per-call values (e.g. ``user_id``) are forwarded to the event. + + Example:: + + @track_tokens(emitter, + agent_name="chat", + model_deployment_name=settings.model, + dimension_args={"user_id": "user_id", + "session_id": "session_id"}) + async def run_chat(prompt, *, user_id, session_id): ... + """ + + dim_args = dict(dimension_args or {}) + + def _decorator(fn: Callable[..., Any]): + is_coro = _is_coroutine_function(fn) + + if is_coro: + @functools.wraps(fn) + async def _aw(*args, **kwargs) -> Any: + with _scope_for(kwargs) as scope: + result = await fn(*args, **kwargs) + scope.add(result) + return result + return _aw + + @functools.wraps(fn) + def _sw(*args, **kwargs) -> Any: + with _scope_for(kwargs) as scope: + result = fn(*args, **kwargs) + scope.add(result) + return result + return _sw + + def _scope_for(call_kwargs: Mapping[str, Any]) -> TokenUsageScope: + dimensions = { + prop: call_kwargs.get(kw) + for prop, kw in dim_args.items() + if call_kwargs.get(kw) is not None + } + return TokenUsageScope( + emitter, + agent_name=agent_name, + model_deployment_name=model_deployment_name, + additional_agents=additional_agents, + emit_user_event=emit_user_event, + emit_team_event=emit_team_event, + **dimensions, + ) + + return _decorator + + +def _is_coroutine_function(fn: Callable[..., Any]) -> bool: + return asyncio.iscoroutinefunction(fn) + + +__all__ = [ + "EVENT_SUMMARY", + "EVENT_AGENT", + "EVENT_MODEL", + "EVENT_USER", + "EVENT_TEAM", + "EVENT_SPEECH", + "TokenUsage", + "TokenUsageEmitter", + "TokenUsageScope", + "track_tokens", + "extract_usage", + "extract_usage_from_dict", + "extract_usage_from_stream_chunk", + "extract_realtime_usage", + "detect_invoked_tools", +] diff --git a/src/processor/src/tests/test_llm_token_telemetry.py b/src/processor/src/tests/test_llm_token_telemetry.py new file mode 100644 index 00000000..6a24f5b1 --- /dev/null +++ b/src/processor/src/tests/test_llm_token_telemetry.py @@ -0,0 +1,572 @@ +"""Unit tests for app.utils.llm_token_telemetry. + +Covers: +- TokenUsage arithmetic and realtime sub-fields +- All extractors (dict / object / raw_representation / aggregated messages / + streaming chunks / realtime / Mock-input safety) +- detect_invoked_tools +- TokenUsageEmitter: enabled/disabled, sink-throws-doesn't-propagate, + static_dimensions merge, all typed emitters, emit_all distinct models +- TokenUsageScope: happy path, exception in body still emits, multi-add +""" +from __future__ import annotations + +import logging +from unittest.mock import Mock + +import pytest + +from app.utils.llm_token_telemetry import ( + EVENT_AGENT, + EVENT_MODEL, + EVENT_SPEECH, + EVENT_SUMMARY, + TokenUsage, + TokenUsageEmitter, + TokenUsageScope, + detect_invoked_tools, + extract_realtime_usage, + extract_usage, + extract_usage_from_dict, + extract_usage_from_stream_chunk, +) + + +# --------------------------------------------------------------------------- +# TokenUsage +# --------------------------------------------------------------------------- +class TestTokenUsage: + def test_has_any_false_when_zero(self): + assert TokenUsage().has_any is False + + def test_has_any_true_when_any_nonzero(self): + assert TokenUsage(input_tokens=1).has_any is True + assert TokenUsage(total_tokens=5).has_any is True + + def test_addition_basic(self): + a = TokenUsage(1, 2, 3) + b = TokenUsage(4, 5, 9) + assert a + b == TokenUsage(5, 7, 12) + + def test_addition_realtime_subfields(self): + a = TokenUsage(1, 2, 3, input_audio_tokens=10) + b = TokenUsage(4, 5, 9, input_audio_tokens=20, output_audio_tokens=7) + c = a + b + assert c.input_audio_tokens == 30 + assert c.output_audio_tokens == 7 # None + 7 -> 7 + + def test_addition_returns_notimplemented_for_other_types(self): + assert TokenUsage(1).__add__("nope") is NotImplemented + + def test_to_event_props_omits_none_subfields(self): + props = TokenUsage(1, 2, 3).to_event_props() + assert props == {"input_tokens": "1", "output_tokens": "2", "total_tokens": "3"} + + def test_to_event_props_includes_realtime_when_present(self): + props = TokenUsage(1, 2, 3, input_audio_tokens=4).to_event_props() + assert props["input_audio_tokens"] == "4" + + +# --------------------------------------------------------------------------- +# extract_usage_from_dict +# --------------------------------------------------------------------------- +class TestExtractFromDict: + @pytest.mark.parametrize("data,expected", [ + ({"prompt_tokens": 12, "completion_tokens": 8}, (12, 8, 20)), + ({"input_tokens": 5, "output_tokens": 7, "total_tokens": 12}, (5, 7, 12)), + ({"input_token_count": 3, "output_token_count": 4}, (3, 4, 7)), + ({"promptTokens": 1, "completionTokens": 2, "totalTokens": 3}, (1, 2, 3)), + ]) + def test_aliases(self, data, expected): + u = extract_usage_from_dict(data) + assert (u.input_tokens, u.output_tokens, u.total_tokens) == expected + + def test_none_returns_none(self): + assert extract_usage_from_dict(None) is None + + def test_empty_returns_none(self): + assert extract_usage_from_dict({}) is None + + def test_total_falls_back_to_sum(self): + u = extract_usage_from_dict({"input_tokens": 4, "output_tokens": 6}) + assert u.total_tokens == 10 + + def test_string_digits_coerced(self): + u = extract_usage_from_dict({"input_tokens": "10", "output_tokens": "20"}) + assert u.input_tokens == 10 + assert u.output_tokens == 20 + + +# --------------------------------------------------------------------------- +# extract_usage (object shapes) +# --------------------------------------------------------------------------- +class _Bag: + """Minimal attribute bag (acts like an SDK model object).""" + pass + + +class TestExtractUsage: + def test_usage_details_dict(self): + r = _Bag() + r.usage_details = {"input_token_count": 5, "output_token_count": 7} + u = extract_usage(r) + assert u.total_tokens == 12 + + def test_usage_details_object(self): + r = _Bag() + details = _Bag() + details.input_token_count = 5 + details.output_token_count = 7 + details.total_token_count = 12 + r.usage_details = details + u = extract_usage(r) + assert u.total_tokens == 12 + + def test_raw_representation_openai_shape(self): + r = _Bag() + raw = _Bag() + raw.usage = {"prompt_tokens": 3, "completion_tokens": 4, "total_tokens": 7} + r.raw_representation = raw + u = extract_usage(r) + assert (u.input_tokens, u.output_tokens, u.total_tokens) == (3, 4, 7) + + def test_aggregated_messages(self): + r = _Bag() + msg = _Bag() + c1 = _Bag() + c1.usage_details = {"input_tokens": 2, "output_tokens": 3} + c2 = _Bag() + c2.usage_details = {"input_tokens": 4, "output_tokens": 1} + msg.contents = [c1, c2] + r.messages = [msg] + u = extract_usage(r) + assert u.input_tokens == 6 + assert u.output_tokens == 4 + + def test_none_input_returns_none(self): + assert extract_usage(None) is None + + def test_no_usage_returns_none(self): + assert extract_usage(_Bag()) is None + + def test_mock_input_does_not_raise(self): + """Mock objects expose every attribute as another Mock -- previously + this caused TypeError on iteration of .messages.""" + m = Mock() + # Should silently return None, never raise. + assert extract_usage(m) is None + + +# --------------------------------------------------------------------------- +# extract_usage_from_stream_chunk +# --------------------------------------------------------------------------- +class TestStreamChunk: + def test_chunk_with_metadata_usage(self): + c = _Bag() + c.metadata = {"usage": {"input_tokens": 1, "output_tokens": 2}} + u = extract_usage_from_stream_chunk(c) + assert u.input_tokens == 1 + assert u.output_tokens == 2 + + def test_no_usage_returns_none(self): + assert extract_usage_from_stream_chunk(_Bag()) is None + + +# --------------------------------------------------------------------------- +# extract_realtime_usage +# --------------------------------------------------------------------------- +class TestRealtime: + def test_basic(self): + r = _Bag() + r.usage = { + "input_tokens": 3, "output_tokens": 4, "total_tokens": 7, + "input_token_details": {"audio_tokens": 2, "text_tokens": 1, "cached_tokens": 0}, + "output_token_details": {"audio_tokens": 4, "text_tokens": 0}, + } + u = extract_realtime_usage(r) + assert u.input_audio_tokens == 2 + assert u.output_audio_tokens == 4 + assert u.total_tokens == 7 + + def test_total_derived_when_missing(self): + r = _Bag() + r.usage = {"input_tokens": 3, "output_tokens": 4} + u = extract_realtime_usage(r) + assert u.total_tokens == 7 + + def test_no_usage_returns_none(self): + assert extract_realtime_usage(_Bag()) is None + + +# --------------------------------------------------------------------------- +# detect_invoked_tools +# --------------------------------------------------------------------------- +class TestDetectInvokedTools: + def test_finds_function_calls(self): + r = _Bag() + c1 = _Bag() + c1.type = "function_call" + c1.name = "product_agent" + c2 = _Bag() + c2.type = "text" + c2.name = "n/a" + c3 = _Bag() + c3.type = "function_call" + c3.name = "policy_agent" + msg = _Bag() + msg.contents = [c1, c2, c3] + r.messages = [msg] + assert detect_invoked_tools(r) == {"product_agent", "policy_agent"} + + def test_empty_when_no_messages(self): + assert detect_invoked_tools(_Bag()) == set() + + def test_mock_input_safe(self): + assert detect_invoked_tools(Mock()) == set() + + def test_skips_function_calls_without_name(self): + r = _Bag() + c = _Bag() + c.type = "function_call" + c.name = None + msg = _Bag() + msg.contents = [c] + r.messages = [msg] + assert detect_invoked_tools(r) == set() + + +# --------------------------------------------------------------------------- +# TokenUsageEmitter +# --------------------------------------------------------------------------- +class TestEmitter: + def _make(self, **kw): + captured: list[tuple[str, dict]] = [] + kw.setdefault("connection_string", "fake-conn") + kw.setdefault("event_sink", lambda n, p: captured.append((n, dict(p)))) + em = TokenUsageEmitter(**kw) + return em, captured + + def test_disabled_when_no_connection_string(self): + em = TokenUsageEmitter(connection_string="", event_sink=lambda *a: None) + assert em.enabled is False + + def test_disabled_when_no_sink(self): + em = TokenUsageEmitter(connection_string="x", event_sink=None) + # _default_event_sink may or may not be available; force-disable: + em._sink = None + assert em.enabled is False + + def test_static_dimensions_prestringified_and_merged(self): + em, captured = self._make(static_dimensions={"app": "x", "tenant": 42}) + em.emit("X", user_id="u1") + name, props = captured[0] + assert name == "X" + assert props["app"] == "x" + assert props["tenant"] == "42" # stringified + assert props["user_id"] == "u1" + + def test_call_dimension_overrides_static(self): + em, captured = self._make(static_dimensions={"app": "default"}) + em.emit("X", app="override") + assert captured[0][1]["app"] == "override" + + def test_none_dimension_dropped(self): + em, captured = self._make() + em.emit("X", user_id=None, session_id="s1") + assert "user_id" not in captured[0][1] + assert captured[0][1]["session_id"] == "s1" + + def test_sink_exception_does_not_propagate(self, caplog): + def boom(_n, _p): + raise RuntimeError("sink broken") + em = TokenUsageEmitter(connection_string="x", event_sink=boom) + with caplog.at_level(logging.WARNING): + em.emit("X") # must not raise + + def test_emit_agent_skips_zero_usage(self): + em, captured = self._make() + em.emit_agent(agent_name="a", model_deployment_name="m", usage=TokenUsage()) + assert captured == [] + + def test_emit_agent_populates_props(self): + em, captured = self._make() + em.emit_agent(agent_name="chat", model_deployment_name="gpt-4o", + usage=TokenUsage(10, 20, 30), user_id="u") + name, props = captured[0] + assert name == EVENT_AGENT + assert props["agent_name"] == "chat" + assert props["model_deployment_name"] == "gpt-4o" + assert props["total_tokens"] == "30" + assert props["user_id"] == "u" + + def test_emit_all_emits_summary_agent_and_per_distinct_model(self): + em, captured = self._make() + em.emit_all( + agent_name="orchestrator", + model_deployment_name="gpt-4o", + usage=TokenUsage(10, 20, 30), + additional_agents={"tool_a": "gpt-4o", "tool_b": "gpt-35"}, + user_id="u1", + ) + names = [n for n, _ in captured] + # exactly one summary + one agent + two model events (gpt-4o, gpt-35) + assert names.count(EVENT_SUMMARY) == 1 + assert names.count(EVENT_AGENT) == 1 + assert names.count(EVENT_MODEL) == 2 + # summary records agent + model counts + summary = next(p for n, p in captured if n == EVENT_SUMMARY) + assert summary["agent_count"] == "3" + assert summary["model_count"] == "2" + assert summary["total_input_tokens"] == "10" + + def test_emit_speech_includes_audio_subfields(self): + em, captured = self._make() + em.emit_speech( + model_deployment_name="gpt-4o-realtime", + source="voice_chat", + usage=TokenUsage(1, 2, 3, input_audio_tokens=5, output_audio_tokens=6), + ) + name, props = captured[0] + assert name == EVENT_SPEECH + assert props["source"] == "voice_chat" + assert props["input_audio_tokens"] == "5" + assert props["output_audio_tokens"] == "6" + + +# --------------------------------------------------------------------------- +# Pricing / cost computation +# --------------------------------------------------------------------------- +class TestPricing: + def _make(self, pricing): + captured: list[tuple[str, dict]] = [] + em = TokenUsageEmitter( + connection_string="x", + event_sink=lambda n, p: captured.append((n, dict(p))), + pricing=pricing, + ) + return em, captured + + def test_cost_attached_to_agent_event(self): + em, captured = self._make({"gpt-4o": (0.0025, 0.01)}) + em.emit_agent(agent_name="a", model_deployment_name="gpt-4o", + usage=TokenUsage(1000, 500, 1500)) + # 1000 * 0.0025/1k + 500 * 0.01/1k = 0.0025 + 0.005 = 0.0075 + assert captured[0][1]["estimated_cost_usd"] == "0.007500" + + def test_cost_case_insensitive_model_lookup(self): + em, captured = self._make({"GPT-4o": (0.001, 0.001)}) + em.emit_model(model_deployment_name="gpt-4o", + usage=TokenUsage(1000, 1000, 2000)) + assert "estimated_cost_usd" in captured[0][1] + + def test_no_cost_when_model_unknown(self): + em, captured = self._make({"gpt-4o": (0.001, 0.001)}) + em.emit_agent(agent_name="a", model_deployment_name="gpt-mystery", + usage=TokenUsage(10, 10, 20)) + assert "estimated_cost_usd" not in captured[0][1] + + def test_summary_picks_up_cost_via_emit_all(self): + em, captured = self._make({"gpt-4o": (0.0025, 0.01)}) + em.emit_all(agent_name="chat", model_deployment_name="gpt-4o", + usage=TokenUsage(1000, 500, 1500)) + summary = next(p for n, p in captured if n == EVENT_SUMMARY) + assert summary["estimated_cost_usd"] == "0.007500" + + def test_malformed_pricing_entry_ignored(self, caplog): + with caplog.at_level(logging.WARNING): + em = TokenUsageEmitter( + connection_string="x", + event_sink=lambda *a: None, + pricing={"bad-model": "not-a-tuple"}, # type: ignore[dict-item] + ) + # Emitter still constructs; bad entry skipped. + assert "bad-model" not in em._pricing + + +# --------------------------------------------------------------------------- +# user_id PII hashing +# --------------------------------------------------------------------------- +class TestUserIdHasher: + def _make(self, hasher): + captured: list[tuple[str, dict]] = [] + em = TokenUsageEmitter( + connection_string="x", + event_sink=lambda n, p: captured.append((n, dict(p))), + user_id_hasher=hasher, + ) + return em, captured + + def test_hasher_applied_to_call_kwargs(self): + em, captured = self._make(lambda v: f"H({v})") + em.emit("X", user_id="alice") + assert captured[0][1]["user_id"] == "H(alice)" + + def test_hasher_applied_to_static_dimensions_at_construction(self): + em = TokenUsageEmitter( + connection_string="x", + event_sink=lambda *a: None, + user_id_hasher=lambda v: f"H({v})", + static_dimensions={"user_id": "bob"}, + ) + assert em._static["user_id"] == "H(bob)" + + def test_hasher_exception_falls_back_to_raw(self, caplog): + def boom(_v): + raise RuntimeError("hasher broken") + em, captured = self._make(boom) + with caplog.at_level(logging.WARNING): + em.emit("X", user_id="alice") + # Falls back to original value -- never breaks telemetry. + assert captured[0][1]["user_id"] == "alice" + + def test_no_hasher_passes_through(self): + em, captured = self._make(None) + em.emit("X", user_id="alice") + assert captured[0][1]["user_id"] == "alice" + + def test_empty_user_id_not_hashed_or_emitted(self): + em, captured = self._make(lambda v: f"H({v})") + em.emit("X", user_id="") + # Empty user_id should be dropped, not hashed to "H()". + assert "user_id" not in captured[0][1] + + +# --------------------------------------------------------------------------- +# Sampling +# --------------------------------------------------------------------------- +class TestSampling: + def _make(self, rate): + captured: list[tuple[str, dict]] = [] + em = TokenUsageEmitter( + connection_string="x", + event_sink=lambda n, p: captured.append((n, dict(p))), + sample_rate=rate, + ) + return em, captured + + def test_rate_clamped_to_unit_interval(self): + assert TokenUsageEmitter(connection_string="x", sample_rate=-0.5, + event_sink=lambda *a: None).sample_rate == 0.0 + assert TokenUsageEmitter(connection_string="x", sample_rate=2.0, + event_sink=lambda *a: None).sample_rate == 1.0 + + def test_invalid_rate_defaults_to_one(self): + em = TokenUsageEmitter(connection_string="x", sample_rate="nope", # type: ignore[arg-type] + event_sink=lambda *a: None) + assert em.sample_rate == 1.0 + + def test_zero_rate_drops_agent_event(self): + em, captured = self._make(0.0) + em.emit_agent(agent_name="a", model_deployment_name="m", + usage=TokenUsage(1, 2, 3)) + assert captured == [] + + def test_zero_rate_still_emits_summary(self): + em, captured = self._make(0.0) + em.emit_summary(usage=TokenUsage(1, 2, 3)) + assert captured and captured[0][0] == EVENT_SUMMARY + + def test_summary_records_sample_rate(self): + em, captured = self._make(0.25) + em.emit_summary(usage=TokenUsage(1, 2, 3)) + assert captured[0][1]["sample_rate"] == "0.2500" + + def test_emit_all_with_zero_rate_only_emits_summary(self): + em, captured = self._make(0.0) + em.emit_all(agent_name="chat", model_deployment_name="gpt-4o", + usage=TokenUsage(10, 20, 30)) + assert [n for n, _ in captured] == [EVENT_SUMMARY] + + def test_full_rate_emits_everything(self): + em, captured = self._make(1.0) + em.emit_all(agent_name="chat", model_deployment_name="gpt-4o", + usage=TokenUsage(10, 20, 30), + additional_agents={"a2": "gpt-35"}) + names = [n for n, _ in captured] + assert EVENT_SUMMARY in names + assert EVENT_AGENT in names + assert names.count(EVENT_MODEL) == 2 + + +# --------------------------------------------------------------------------- +# TokenUsageScope (continued) +# --------------------------------------------------------------------------- +class TestScope: + def _emitter(self): + captured: list[tuple[str, dict]] = [] + em = TokenUsageEmitter( + connection_string="x", + event_sink=lambda n, p: captured.append((n, dict(p))), + ) + return em, captured + + def test_happy_path_emits_on_exit(self): + em, captured = self._emitter() + r = _Bag() + r.usage_details = {"input_tokens": 1, "output_tokens": 2, "total_tokens": 3} + with TokenUsageScope(em, agent_name="a", model_deployment_name="m") as s: + s.add(r) + assert any(n == EVENT_SUMMARY for n, _ in captured) + assert any(n == EVENT_AGENT for n, _ in captured) + + def test_multi_add_accumulates(self): + em, captured = self._emitter() + r1 = _Bag() + r1.usage_details = {"input_tokens": 1, "output_tokens": 2} + r2 = _Bag() + r2.usage_details = {"input_tokens": 4, "output_tokens": 5} + with TokenUsageScope(em, agent_name="a", model_deployment_name="m") as s: + s.add(r1) + s.add(r2) + agent = next(p for n, p in captured if n == EVENT_AGENT) + assert agent["input_tokens"] == "5" + assert agent["output_tokens"] == "7" + assert agent["total_tokens"] == "12" + + def test_exception_in_body_still_emits(self): + em, captured = self._emitter() + r = _Bag() + r.usage_details = {"input_tokens": 1, "output_tokens": 2, "total_tokens": 3} + with pytest.raises(ValueError): + with TokenUsageScope(em, agent_name="a", model_deployment_name="m") as s: + s.add(r) + raise ValueError("boom") + # Emission still happened + assert any(n == EVENT_AGENT for n, _ in captured) + + def test_add_with_mock_does_not_raise(self): + em, _ = self._emitter() + with TokenUsageScope(em, agent_name="a", model_deployment_name="m") as s: + assert s.add(Mock()) is None + + def test_zero_usage_does_not_emit(self): + em, captured = self._emitter() + with TokenUsageScope(em, agent_name="a", model_deployment_name="m"): + pass + assert captured == [] + + def test_dimensions_flow_to_events(self): + em, captured = self._emitter() + r = _Bag() + r.usage_details = {"input_tokens": 1, "output_tokens": 2, "total_tokens": 3} + with TokenUsageScope(em, agent_name="a", model_deployment_name="m", + user_id="u1", session_id="s1") as s: + s.add(r) + for _, p in captured: + assert p["user_id"] == "u1" + assert p["session_id"] == "s1" + + def test_additional_agents_after_scope_open(self): + em, captured = self._emitter() + r = _Bag() + r.usage_details = {"input_tokens": 1, "output_tokens": 2, "total_tokens": 3} + with TokenUsageScope(em, agent_name="orchestrator", + model_deployment_name="gpt-4o") as s: + s.add(r) + # Mutate additional_agents after the call -- mirrors the + # detect_invoked_tools usage pattern. + s.additional_agents["tool_a"] = "gpt-35" + model_events = [p for n, p in captured if n == EVENT_MODEL] + models = {p["model_deployment_name"] for p in model_events} + assert models == {"gpt-4o", "gpt-35"} + diff --git a/src/processor/src/utils/llm_token_telemetry.py b/src/processor/src/utils/llm_token_telemetry.py new file mode 100644 index 00000000..b3035fc8 --- /dev/null +++ b/src/processor/src/utils/llm_token_telemetry.py @@ -0,0 +1,935 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Cross-accelerator LLM token-usage telemetry helpers. + +A single, dependency-light helper module that can be dropped into any Microsoft +Solution Accelerator to capture LLM token usage and emit standardized custom +events to Application Insights. + +Why this file exists +-------------------- +Seven solution accelerators have independently shipped near-identical +``token_usage_utils.py`` modules (see PRs: content-generation #860, CKM #933, +content-processing #586, Container-Migration #257, agentic-data-foundation +#383, customer-chatbot #218, MACAE #1003). They all: + +* extract token counts from agent_framework / Azure OpenAI responses, +* emit the same three custom events (``LLM_Token_Usage_Summary``, + ``LLM_Agent_Token_Usage``, ``LLM_Model_Token_Usage``), +* defensively swallow telemetry errors, +* duplicate the same KQL queries and Azure Workbook. + +This module consolidates the union of those behaviours behind one stable API +so each accelerator can replace its bespoke helper with an import. + +Public API +---------- +- ``TokenUsage`` -- immutable dataclass for counts +- ``extract_usage(obj)`` -- agent_framework run result / message +- ``extract_usage_from_dict(d)`` -- raw dict from any SDK +- ``extract_usage_from_stream_chunk`` -- streaming chunks +- ``extract_realtime_usage(resp)`` -- Azure AI Voice Live response.done +- ``TokenUsageEmitter`` -- emits the three events + optional + per-user / per-team / speech events +- ``TokenUsageScope`` -- context-manager that accumulates and + auto-emits on exit +- ``track_tokens`` -- decorator wrapper around the scope + +Design rules +------------ +* Telemetry NEVER raises. Extraction failures return ``None``; emission + failures are logged at WARNING. +* No hard dependency on ``azure-monitor-events-extension``; if absent the + emitter degrades to logging only. +* Arbitrary correlation dimensions are passed as ``**dimensions`` kwargs and + surface verbatim as custom-event properties. This is how each accelerator + attaches its own keys (``conversation_id``, ``process_id``, ``team_name``, + ``file_name``, ``tenant``, etc.) without forking the helper. +""" +from __future__ import annotations + +import asyncio +import functools +import logging +import os +import random +from contextlib import AbstractContextManager +from dataclasses import dataclass, field +from typing import Any, Callable, Iterable, Mapping, Optional +from unittest.mock import NonCallableMock + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Event-name constants -- keep these stable; KQL queries and workbooks bind +# to these exact strings. +# --------------------------------------------------------------------------- +EVENT_SUMMARY = "LLM_Token_Usage_Summary" +EVENT_AGENT = "LLM_Agent_Token_Usage" +EVENT_MODEL = "LLM_Model_Token_Usage" +EVENT_USER = "LLM_User_Token_Usage" +EVENT_TEAM = "LLM_Team_Token_Usage" +EVENT_SPEECH = "Speech_Usage" + + +# Token-count field aliases observed across model providers / SDK versions. +_INPUT_KEYS = ( + "input_token_count", + "input_tokens", + "prompt_tokens", + "promptTokens", +) +_OUTPUT_KEYS = ( + "output_token_count", + "output_tokens", + "completion_tokens", + "completionTokens", +) +_TOTAL_KEYS = ( + "total_token_count", + "total_tokens", + "totalTokens", +) + + +# --------------------------------------------------------------------------- +# Data model +# --------------------------------------------------------------------------- +@dataclass(frozen=True) +class TokenUsage: + """Normalized token-usage record.""" + + input_tokens: int = 0 + output_tokens: int = 0 + total_tokens: int = 0 + + # Optional realtime / voice fields (None unless populated) + input_audio_tokens: Optional[int] = None + input_text_tokens: Optional[int] = None + input_cached_tokens: Optional[int] = None + output_audio_tokens: Optional[int] = None + output_text_tokens: Optional[int] = None + + @property + def has_any(self) -> bool: + return bool(self.input_tokens or self.output_tokens or self.total_tokens) + + def __add__(self, other: "TokenUsage") -> "TokenUsage": + if not isinstance(other, TokenUsage): + return NotImplemented + + def _sum(a: Optional[int], b: Optional[int]) -> Optional[int]: + if a is None and b is None: + return None + return (a or 0) + (b or 0) + + return TokenUsage( + input_tokens=self.input_tokens + other.input_tokens, + output_tokens=self.output_tokens + other.output_tokens, + total_tokens=self.total_tokens + other.total_tokens, + input_audio_tokens=_sum(self.input_audio_tokens, other.input_audio_tokens), + input_text_tokens=_sum(self.input_text_tokens, other.input_text_tokens), + input_cached_tokens=_sum(self.input_cached_tokens, other.input_cached_tokens), + output_audio_tokens=_sum(self.output_audio_tokens, other.output_audio_tokens), + output_text_tokens=_sum(self.output_text_tokens, other.output_text_tokens), + ) + + def to_event_props(self) -> dict[str, str]: + """Stringified property bag suitable for App Insights custom events.""" + props: dict[str, str] = { + "input_tokens": str(self.input_tokens), + "output_tokens": str(self.output_tokens), + "total_tokens": str(self.total_tokens), + } + for name in ( + "input_audio_tokens", + "input_text_tokens", + "input_cached_tokens", + "output_audio_tokens", + "output_text_tokens", + ): + value = getattr(self, name) + if value is not None: + props[name] = str(value) + return props + + +# --------------------------------------------------------------------------- +# Low-level coercion helpers +# --------------------------------------------------------------------------- +def _to_int(value: Any, default: int = 0) -> int: + """Best-effort int conversion; bool excluded; never raises.""" + if value is None or isinstance(value, bool): + return default + if isinstance(value, int): + return value + if isinstance(value, float): + return int(value) + if isinstance(value, str): + s = value.strip() + if s.isdigit(): + return int(s) + try: + return int(value) + except (TypeError, ValueError): + return default + + +def _get(obj: Any, key: str, default: Any = None) -> Any: + """Read an attribute or dict key uniformly.""" + if obj is None: + return default + if isinstance(obj, Mapping): + return obj.get(key, default) + return getattr(obj, key, default) + + +def _is_iterable(obj: Any) -> bool: + """True only for real iterables (lists/tuples/sets/generators), NOT for + arbitrary objects (e.g. ``unittest.mock.Mock``) that happen to expose + ``__iter__`` but blow up on iteration.""" + if obj is None: + return False + if isinstance(obj, (list, tuple, set, frozenset)): + return True + # Strings are iterable but never the right answer for "messages". + if isinstance(obj, (str, bytes, bytearray, Mapping)): + return False + # Fall back to a duck-typed check, but reject Mock instances which would + # otherwise pretend to support iteration. + if isinstance(obj, NonCallableMock): + return False + return hasattr(obj, "__iter__") + + +def _read_counts(usage_obj: Any) -> Optional[TokenUsage]: + """Read ``input/output/total`` from any usage-bearing object/dict.""" + if usage_obj is None: + return None + + inp = out = tot = 0 + for k in _INPUT_KEYS: + v = _get(usage_obj, k) + if v: + inp = _to_int(v) + break + for k in _OUTPUT_KEYS: + v = _get(usage_obj, k) + if v: + out = _to_int(v) + break + for k in _TOTAL_KEYS: + v = _get(usage_obj, k) + if v: + tot = _to_int(v) + break + + if tot == 0 and (inp or out): + tot = inp + out + if not (inp or out or tot): + return None + return TokenUsage(input_tokens=inp, output_tokens=out, total_tokens=tot) + + +# --------------------------------------------------------------------------- +# Extraction -- public +# --------------------------------------------------------------------------- +def extract_usage(result: Any) -> Optional[TokenUsage]: + """Extract usage from an agent_framework run result, ChatMessage, or + OpenAI-style ChatCompletion. + + Checks (in order): + 1. ``result.usage_details`` or ``result.usage`` + 2. ``result.raw_representation.usage`` (OpenAI ChatCompletion shape) + 3. Aggregated ``result.messages[*].contents[*].usage_details`` + + Never raises -- returns ``None`` on any unexpected shape. + """ + if result is None: + return None + + try: + for attr in ("usage_details", "usage"): + found = _read_counts(_get(result, attr)) + if found: + return found + + raw = _get(result, "raw_representation") + if raw is not None: + found = _read_counts(_get(raw, "usage")) + if found: + return found + + aggregated = TokenUsage() + found_any = False + messages = _get(result, "messages") + if not _is_iterable(messages): + return None + for msg in messages: + contents = _get(msg, "contents") + if not _is_iterable(contents): + continue + for content in contents: + usage = _get(content, "usage_details") or _get(content, "usage") + piece = _read_counts(usage) + if piece: + aggregated = aggregated + piece + found_any = True + return aggregated if found_any else None + except Exception as exc: + logger.debug("extract_usage failed: %s", exc, exc_info=True) + return None + + +def extract_usage_from_dict(data: Any) -> Optional[TokenUsage]: + """Extract from a raw dict / SDK usage object.""" + return _read_counts(data) + + +def extract_usage_from_stream_chunk(chunk: Any) -> Optional[TokenUsage]: + """Streaming chunks: try the top-level shape, then ``chunk.metadata.usage``.""" + found = extract_usage(chunk) + if found: + return found + metadata = _get(chunk, "metadata") + if metadata is not None: + return _read_counts(_get(metadata, "usage")) + return None + + +def extract_realtime_usage(response_obj: Any) -> Optional[TokenUsage]: + """Azure AI Voice Live ``response.done`` payload extractor. + + Includes audio / text / cached sub-counts when present. + """ + usage = _get(response_obj, "usage") + if usage is None: + return None + + inp = _to_int(_get(usage, "input_tokens")) + out = _to_int(_get(usage, "output_tokens")) + tot = _to_int(_get(usage, "total_tokens")) + if tot == 0 and (inp or out): + tot = inp + out + + in_details = _get(usage, "input_token_details") or {} + out_details = _get(usage, "output_token_details") or {} + + record = TokenUsage( + input_tokens=inp, + output_tokens=out, + total_tokens=tot, + input_audio_tokens=_to_int(_get(in_details, "audio_tokens")), + input_text_tokens=_to_int(_get(in_details, "text_tokens")), + input_cached_tokens=_to_int(_get(in_details, "cached_tokens")), + output_audio_tokens=_to_int(_get(out_details, "audio_tokens")), + output_text_tokens=_to_int(_get(out_details, "text_tokens")), + ) + # Only return if at least one non-zero count surfaced. + if record.has_any or any( + v for v in ( + record.input_audio_tokens, + record.input_text_tokens, + record.input_cached_tokens, + record.output_audio_tokens, + record.output_text_tokens, + ) + ): + return record + return None + + +# --------------------------------------------------------------------------- +# Tool / sub-agent attribution +# --------------------------------------------------------------------------- +def detect_invoked_tools(result: Any) -> set[str]: + """Return the set of tool/function names invoked in an agent result, + inferred from ``function_call`` content items. + + Used by orchestrators that expose sub-agents via ``.as_tool()`` to attribute + token usage only to the sub-agents that were actually called. Never raises. + """ + invoked: set[str] = set() + try: + messages = _get(result, "messages") + if not _is_iterable(messages): + return invoked + for msg in messages: + contents = _get(msg, "contents") + if not _is_iterable(contents): + continue + for content in contents: + if _get(content, "type") == "function_call": + name = _get(content, "name") + if name: + invoked.add(str(name)) + except Exception as exc: + logger.debug("detect_invoked_tools failed: %s", exc, exc_info=True) + return invoked + + +# --------------------------------------------------------------------------- +# Event sink (optional Application Insights dependency) +# --------------------------------------------------------------------------- +EventSink = Callable[[str, Mapping[str, str]], None] + + +def _default_event_sink() -> Optional[EventSink]: + """Return ``azure.monitor.events.extension.track_event`` if importable, + else ``None``. Resolved lazily so the helper still works in unit tests + without the dependency installed.""" + try: + from azure.monitor.events.extension import track_event # type: ignore + except Exception: # pragma: no cover - optional dep + return None + return track_event + + +# --------------------------------------------------------------------------- +# Emitter +# --------------------------------------------------------------------------- +class TokenUsageEmitter: + """Emit standardized token-usage custom events. + + Parameters + ---------- + connection_string: + Application Insights connection string. If ``None`` (default), the + ``APPLICATIONINSIGHTS_CONNECTION_STRING`` env var is consulted. When + no connection string is configured the emitter logs and skips the + ``track_event`` call. + static_dimensions: + Properties merged into every event (e.g. ``{"app": "customer-chatbot"}``). + event_sink: + Callable ``(event_name, props_dict) -> None``. Defaults to + ``azure.monitor.events.extension.track_event``. Override in tests. + pricing: + Optional mapping ``{model_deployment_name -> (usd_per_1k_input, + usd_per_1k_output)}``. When provided, an ``estimated_cost_usd`` + property is attached to agent / model / summary events. Model lookup + is case-insensitive. Use this to avoid hard-coding rates in KQL. + user_id_hasher: + Optional callable ``str -> str`` applied to any ``user_id`` value + before it leaves the emitter. Use this to satisfy PII / GDPR + requirements (e.g. HMAC-SHA256 with a tenant-scoped salt). Applied + to both ``static_dimensions['user_id']`` (at construction) and + per-call ``user_id`` kwargs. + sample_rate: + Fraction of high-cardinality events (agent / model / user / team / + speech) actually shipped, in ``[0.0, 1.0]``. The cheap **summary + event always fires** regardless of sample_rate so per-request totals + remain accurate; only the per-dimension breakdown is sampled. + Defaults to ``1.0`` (no sampling). + logger: + Override the module logger. + """ + + def __init__( + self, + *, + connection_string: Optional[str] = None, + static_dimensions: Optional[Mapping[str, Any]] = None, + event_sink: Optional[EventSink] = None, + pricing: Optional[Mapping[str, tuple[float, float]]] = None, + user_id_hasher: Optional[Callable[[str], str]] = None, + sample_rate: float = 1.0, + logger: Optional[logging.Logger] = None, + ) -> None: + self._cs = connection_string if connection_string is not None else os.getenv( + "APPLICATIONINSIGHTS_CONNECTION_STRING" + ) + self._sink = event_sink if event_sink is not None else _default_event_sink() + self._log = logger or logging.getLogger(__name__) + + # PII hashing applied to user_id everywhere. + self._user_id_hasher = user_id_hasher + + # Sampling clamp to [0, 1]. + try: + sr = float(sample_rate) + except (TypeError, ValueError): + sr = 1.0 + self._sample_rate = max(0.0, min(1.0, sr)) + + # Case-insensitive pricing lookup. Values stored as a (in, out) tuple. + self._pricing: dict[str, tuple[float, float]] = {} + for model, rates in (pricing or {}).items(): + if not model or rates is None: + continue + try: + inp, out = rates + self._pricing[str(model).lower()] = (float(inp), float(out)) + except (TypeError, ValueError): + self._log.warning("Ignoring malformed pricing entry: %s=%r", model, rates) + + # Pre-stringify static dims once. user_id (if present) is hashed here + # so the raw value is never retained on the emitter. + raw_static = dict(static_dimensions or {}) + if "user_id" in raw_static: + raw_static["user_id"] = self._apply_user_id_hash(raw_static["user_id"]) + self._static: dict[str, str] = { + k: ("" if v is None else str(v)) for k, v in raw_static.items() + } + + # -- public surface --------------------------------------------------- + @property + def enabled(self) -> bool: + return bool(self._cs) and self._sink is not None + + @property + def sample_rate(self) -> float: + return self._sample_rate + + # -- internal helpers ------------------------------------------------- + def _apply_user_id_hash(self, value: Any) -> Any: + """Apply the configured user_id_hasher; never raises.""" + if value is None or value == "" or self._user_id_hasher is None: + return value + try: + return self._user_id_hasher(str(value)) + except Exception as exc: # never let hashing break telemetry + self._log.warning("user_id_hasher raised: %s", exc) + return value + + def _should_sample(self) -> bool: + """Sampling decision for high-cardinality events.""" + if self._sample_rate >= 1.0: + return True + if self._sample_rate <= 0.0: + return False + return random.random() < self._sample_rate + + def _cost_props( + self, model_deployment_name: Optional[str], usage: TokenUsage + ) -> dict[str, str]: + """Return ``{'estimated_cost_usd': '...'}`` when pricing is configured + for the given model, else ``{}``. 6-decimal formatting.""" + if not self._pricing or not model_deployment_name: + return {} + rate = self._pricing.get(model_deployment_name.lower()) + if not rate: + return {} + inp_rate, out_rate = rate + cost = (usage.input_tokens * inp_rate + usage.output_tokens * out_rate) / 1000.0 + return {"estimated_cost_usd": f"{cost:.6f}"} + + def _summary_cost_props( + self, + primary_model: Optional[str], + additional_agents: Mapping[str, str], + usage: TokenUsage, + ) -> dict[str, str]: + """Best-effort cost for the summary event: charge full usage at the + primary model's rate (the SDK aggregates sub-agent tokens to the + orchestrator, so apportioning is not possible without per-agent + usage). Falls back to silent skip when no rate is known.""" + if primary_model: + cost = self._cost_props(primary_model, usage) + if cost: + return cost + for m in additional_agents.values(): + cost = self._cost_props(m, usage) + if cost: + return cost + return {} + + def emit(self, event_name: str, **dimensions: Any) -> None: + """Low-level: emit an event with arbitrary properties. + + Non-string values are stringified. ``None`` values are dropped. Any + ``user_id`` value is passed through the configured hasher. + Never raises. + """ + props = dict(self._static) # cheap shallow copy of pre-stringified dims + for k, v in dimensions.items(): + if v is None: + continue + if k == "user_id": + v = self._apply_user_id_hash(v) + if v is None or v == "": + continue + props[k] = v if isinstance(v, str) else str(v) + + if not self.enabled: + self._log.debug( + "App Insights not configured -- skipping event %s (%s)", + event_name, props, + ) + return + try: + self._sink(event_name, props) # type: ignore[misc] + except Exception as exc: # never break the caller + self._log.warning("track_event(%s) failed: %s", event_name, exc) + + # -- typed convenience emitters -------------------------------------- + def emit_agent( + self, + *, + agent_name: str, + model_deployment_name: str, + usage: TokenUsage, + **dimensions: Any, + ) -> None: + if not usage.has_any or not self._should_sample(): + return + self.emit( + EVENT_AGENT, + agent_name=agent_name, + model_deployment_name=model_deployment_name, + **usage.to_event_props(), + **self._cost_props(model_deployment_name, usage), + **dimensions, + ) + + def emit_model( + self, + *, + model_deployment_name: str, + usage: TokenUsage, + **dimensions: Any, + ) -> None: + if not usage.has_any or not self._should_sample(): + return + self.emit( + EVENT_MODEL, + model_deployment_name=model_deployment_name, + **usage.to_event_props(), + **self._cost_props(model_deployment_name, usage), + **dimensions, + ) + + def emit_user( + self, + *, + user_id: str, + usage: TokenUsage, + **dimensions: Any, + ) -> None: + if not usage.has_any or not user_id or not self._should_sample(): + return + self.emit( + EVENT_USER, + user_id=user_id, + **usage.to_event_props(), + **dimensions, + ) + + def emit_team( + self, + *, + team_name: str, + usage: TokenUsage, + **dimensions: Any, + ) -> None: + if not usage.has_any or not team_name or not self._should_sample(): + return + self.emit( + EVENT_TEAM, + team_name=team_name, + **usage.to_event_props(), + **dimensions, + ) + + def emit_summary( + self, + *, + usage: TokenUsage, + agent_count: int = 1, + model_count: int = 1, + primary_model: Optional[str] = None, + additional_agents: Optional[Mapping[str, str]] = None, + **dimensions: Any, + ) -> None: + """The summary event always fires (ignores ``sample_rate``) so per- + request totals remain accurate even when high-cardinality events are + sampled.""" + if not usage.has_any: + return + # Summary historically uses ``total_input_tokens`` / ``total_output_tokens`` + # field names; preserve that wire format for backward compatibility. + props = { + "total_input_tokens": str(usage.input_tokens), + "total_output_tokens": str(usage.output_tokens), + "total_tokens": str(usage.total_tokens), + "agent_count": str(agent_count), + "model_count": str(model_count), + "sample_rate": f"{self._sample_rate:.4f}", + } + # Carry over realtime sub-counts if present. + for k, v in usage.to_event_props().items(): + props.setdefault(k, v) + # Optional total cost. + props.update(self._summary_cost_props(primary_model, additional_agents or {}, usage)) + self.emit(EVENT_SUMMARY, **props, **dimensions) + + def emit_speech( + self, + *, + model_deployment_name: str, + source: str, + usage: TokenUsage, + **dimensions: Any, + ) -> None: + """Voice-Live / realtime speech usage event.""" + if not self._should_sample(): + return + self.emit( + EVENT_SPEECH, + model_deployment_name=model_deployment_name, + source=source, + **usage.to_event_props(), + **self._cost_props(model_deployment_name, usage), + **dimensions, + ) + + # -- combined emit: summary + agent + per-distinct-model --------------- + def emit_all( + self, + *, + agent_name: str, + model_deployment_name: str, + usage: TokenUsage, + additional_agents: Optional[Mapping[str, str]] = None, + emit_user_event: bool = False, + emit_team_event: bool = False, + **dimensions: Any, + ) -> None: + """Convenience: emit summary, agent, and one model event per distinct + model deployment in one shot. + + ``additional_agents`` maps sub-agent name -> its model deployment name + so callers can describe orchestrators that involve multiple agents. + + ``emit_user_event`` / ``emit_team_event`` opt in to the user/team + events; ``user_id`` / ``team_name`` must be present in dimensions for + those to fire. + """ + if not usage.has_any: + return + + agents = {agent_name: model_deployment_name} + if additional_agents: + agents.update({k: v for k, v in additional_agents.items() if k}) + models = {m for m in agents.values() if m} + + self.emit_summary( + usage=usage, + agent_count=len(agents), + model_count=len(models) or 1, + primary_model=model_deployment_name, + additional_agents=additional_agents, + **dimensions, + ) + self.emit_agent( + agent_name=agent_name, + model_deployment_name=model_deployment_name, + usage=usage, + **dimensions, + ) + for model in models: + self.emit_model( + model_deployment_name=model, + usage=usage, + **dimensions, + ) + if emit_user_event and dimensions.get("user_id"): + self.emit_user( + user_id=str(dimensions["user_id"]), + usage=usage, + agent_name=agent_name, + model_deployment_name=model_deployment_name, + ) + if emit_team_event and dimensions.get("team_name"): + self.emit_team( + team_name=str(dimensions["team_name"]), + usage=usage, + agent_name=agent_name, + model_deployment_name=model_deployment_name, + ) + + self._log.info( + "[TOKEN USAGE] agent=%s model=%s input=%d output=%d total=%d %s", + agent_name, + model_deployment_name, + usage.input_tokens, + usage.output_tokens, + usage.total_tokens, + " ".join(f"{k}={v}" for k, v in dimensions.items() if v), + ) + + +# --------------------------------------------------------------------------- +# Scope / decorator sugar +# --------------------------------------------------------------------------- +@dataclass +class TokenUsageScope(AbstractContextManager): + """Accumulate usage across multiple results, then emit on exit. + + Example:: + + with TokenUsageScope(emitter, + agent_name="chat", + model_deployment_name=cfg.model, + user_id=user_id) as scope: + result = await agent.run(prompt) + scope.add(result) # extracts and accumulates + """ + + emitter: TokenUsageEmitter + agent_name: str + model_deployment_name: str + dimensions: dict[str, Any] = field(default_factory=dict) + additional_agents: dict[str, str] = field(default_factory=dict) + emit_user_event: bool = False + emit_team_event: bool = False + usage: TokenUsage = field(default_factory=TokenUsage) + + def __init__( + self, + emitter: TokenUsageEmitter, + *, + agent_name: str, + model_deployment_name: str, + additional_agents: Optional[Mapping[str, str]] = None, + emit_user_event: bool = False, + emit_team_event: bool = False, + **dimensions: Any, + ) -> None: + self.emitter = emitter + self.agent_name = agent_name + self.model_deployment_name = model_deployment_name + self.additional_agents = dict(additional_agents or {}) + self.emit_user_event = emit_user_event + self.emit_team_event = emit_team_event + self.dimensions = dict(dimensions) + self.usage = TokenUsage() + + # -- accumulation ----------------------------------------------------- + def add(self, source: Any) -> Optional[TokenUsage]: + """Extract usage from any supported shape and add to the running total. + + Never raises -- extraction failures return ``None`` and are logged + at DEBUG. + """ + try: + found = extract_usage(source) or extract_usage_from_stream_chunk(source) + except Exception as exc: # belt + braces; extractors are already safe + logger.debug("TokenUsageScope.add failed: %s", exc, exc_info=True) + return None + if found: + self.usage = self.usage + found + return found + + def add_usage(self, usage: TokenUsage) -> None: + self.usage = self.usage + usage + + def add_chunks(self, chunks: Iterable[Any]) -> None: + for c in chunks: + self.add(c) + + # -- context manager -------------------------------------------------- + def __exit__(self, exc_type, exc, tb) -> None: + # Always emit (best-effort) regardless of exception status. + try: + self.emitter.emit_all( + agent_name=self.agent_name, + model_deployment_name=self.model_deployment_name, + usage=self.usage, + additional_agents=self.additional_agents, + emit_user_event=self.emit_user_event, + emit_team_event=self.emit_team_event, + **self.dimensions, + ) + except Exception as emit_exc: # pragma: no cover - belt + braces + logger.warning("TokenUsageScope emit failed: %s", emit_exc) + return None # do not suppress exceptions + + +def track_tokens( + emitter: TokenUsageEmitter, + *, + agent_name: str, + model_deployment_name: str, + dimension_args: Optional[Mapping[str, str]] = None, + additional_agents: Optional[Mapping[str, str]] = None, + emit_user_event: bool = False, + emit_team_event: bool = False, +): + """Decorator: wrap an async or sync function that returns an LLM result. + + ``dimension_args`` maps emitted-property-name -> callable-keyword-argument + name so per-call values (e.g. ``user_id``) are forwarded to the event. + + Example:: + + @track_tokens(emitter, + agent_name="chat", + model_deployment_name=settings.model, + dimension_args={"user_id": "user_id", + "session_id": "session_id"}) + async def run_chat(prompt, *, user_id, session_id): ... + """ + + dim_args = dict(dimension_args or {}) + + def _decorator(fn: Callable[..., Any]): + is_coro = _is_coroutine_function(fn) + + if is_coro: + @functools.wraps(fn) + async def _aw(*args, **kwargs) -> Any: + with _scope_for(kwargs) as scope: + result = await fn(*args, **kwargs) + scope.add(result) + return result + return _aw + + @functools.wraps(fn) + def _sw(*args, **kwargs) -> Any: + with _scope_for(kwargs) as scope: + result = fn(*args, **kwargs) + scope.add(result) + return result + return _sw + + def _scope_for(call_kwargs: Mapping[str, Any]) -> TokenUsageScope: + dimensions = { + prop: call_kwargs.get(kw) + for prop, kw in dim_args.items() + if call_kwargs.get(kw) is not None + } + return TokenUsageScope( + emitter, + agent_name=agent_name, + model_deployment_name=model_deployment_name, + additional_agents=additional_agents, + emit_user_event=emit_user_event, + emit_team_event=emit_team_event, + **dimensions, + ) + + return _decorator + + +def _is_coroutine_function(fn: Callable[..., Any]) -> bool: + return asyncio.iscoroutinefunction(fn) + + +__all__ = [ + "EVENT_SUMMARY", + "EVENT_AGENT", + "EVENT_MODEL", + "EVENT_USER", + "EVENT_TEAM", + "EVENT_SPEECH", + "TokenUsage", + "TokenUsageEmitter", + "TokenUsageScope", + "track_tokens", + "extract_usage", + "extract_usage_from_dict", + "extract_usage_from_stream_chunk", + "extract_realtime_usage", + "detect_invoked_tools", +] diff --git a/src/processor/src/utils/token_usage_tracker.py b/src/processor/src/utils/token_usage_tracker.py index 25e5e0b6..0799058a 100644 --- a/src/processor/src/utils/token_usage_tracker.py +++ b/src/processor/src/utils/token_usage_tracker.py @@ -8,20 +8,30 @@ - Per user/process - Per model deployment -Usage data is emitted to Application Insights as custom events and can be -persisted to Cosmos DB via the TelemetryManager. +ses the cross-accelerator ``llm_token_telemetry`` module for extraction +and emission, keeping this tracker as a thin orchestration-specific layer +that adds thread-safe aggregation and per-step tracking. """ from __future__ import annotations import logging import threading -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any -from utils.event_utils import track_event_if_configured +from utils.llm_token_telemetry import ( + TokenUsage, + TokenUsageEmitter, + extract_usage, + extract_usage_from_dict, + extract_usage_from_stream_chunk, +) logger = logging.getLogger(__name__) +# Module-level emitter instance shared across the processor service. +_emitter = TokenUsageEmitter() + @dataclass class TokenUsageRecord: @@ -60,8 +70,8 @@ class TokenUsageTracker: """Thread-safe tracker that aggregates LLM token usage across multiple dimensions. Accumulates usage per agent, per step (team), per model, and overall per process. - Emits Application Insights custom events for each recorded interaction and - provides summary emission at process completion. + Emits Application Insights custom events via ``TokenUsageEmitter`` from the + cross-accelerator ``llm_token_telemetry`` module. """ def __init__(self, process_id: str, user_id: str = ""): @@ -95,8 +105,8 @@ def record( ) -> None: """Record a single LLM call's token usage. - Accumulates into all relevant dimensions and emits a per-call - Application Insights event. + Accumulates into all relevant dimensions and emits per-call + Application Insights events via ``TokenUsageEmitter``. """ if total_tokens <= 0 and input_tokens <= 0 and output_tokens <= 0: return @@ -132,23 +142,20 @@ def record( self._by_model[model_deployment_name] = AggregatedTokenUsage() self._by_model[model_deployment_name].add(record) - # Emit per-call event to Application Insights - try: - track_event_if_configured( - "LLM_Token_Usage", - { - "process_id": self.process_id, - "user_id": self.user_id, - "agent_name": agent_name, - "step_name": step_name, - "model_deployment_name": model_deployment_name, - "input_tokens": str(input_tokens), - "output_tokens": str(output_tokens), - "total_tokens": str(total_tokens), - }, - ) - except Exception: - logger.debug("Failed to emit per-call token usage event", exc_info=True) + # Emit per-call events via the cross-accelerator emitter + usage = TokenUsage( + input_tokens=input_tokens, + output_tokens=output_tokens, + total_tokens=total_tokens, + ) + _emitter.emit_all( + agent_name=agent_name or "unknown", + model_deployment_name=model_deployment_name or "unknown", + usage=usage, + process_id=self.process_id, + user_id=self.user_id, + step_name=step_name, + ) logger.info( "[TOKEN] Recorded: agent=%s step=%s model=%s input=%d output=%d total=%d | cumulative=%d", @@ -177,72 +184,75 @@ def emit_summary_events(self) -> None: """Emit summary-level Application Insights custom events. Call this at the end of a process/workflow to produce aggregated events - that are easy to query in KQL. + that are easy to query in KQL. Uses ``TokenUsageEmitter`` from + ``llm_token_telemetry`` for all event emission. """ summary = self.get_summary() try: + total = summary["total"] + total_usage = TokenUsage( + input_tokens=total["input_tokens"], + output_tokens=total["output_tokens"], + total_tokens=total["total_tokens"], + ) + # Overall summary - track_event_if_configured( - "LLM_Token_Usage_Summary", - { - "process_id": self.process_id, - "user_id": self.user_id, - "total_input_tokens": str(summary["total"]["input_tokens"]), - "total_output_tokens": str(summary["total"]["output_tokens"]), - "total_tokens": str(summary["total"]["total_tokens"]), - "total_calls": str(summary["total"]["call_count"]), - "agent_count": str(len(summary["by_agent"])), - "model_count": str(len(summary["by_model"])), - "step_count": str(len(summary["by_step"])), - }, + _emitter.emit_summary( + usage=total_usage, + agent_count=len(summary["by_agent"]), + model_count=len(summary["by_model"]), + process_id=self.process_id, + user_id=self.user_id, + total_calls=str(total["call_count"]), + step_count=str(len(summary["by_step"])), ) # Per-agent events - for agent_name, usage in summary["by_agent"].items(): + for agent_name, usage_dict in summary["by_agent"].items(): model = self._agent_model_map.get(agent_name, "") - track_event_if_configured( - "LLM_Agent_Token_Usage", - { - "process_id": self.process_id, - "user_id": self.user_id, - "agent_name": agent_name, - "model_deployment_name": model, - "input_tokens": str(usage["input_tokens"]), - "output_tokens": str(usage["output_tokens"]), - "total_tokens": str(usage["total_tokens"]), - "call_count": str(usage["call_count"]), - }, + agent_usage = TokenUsage( + input_tokens=usage_dict["input_tokens"], + output_tokens=usage_dict["output_tokens"], + total_tokens=usage_dict["total_tokens"], + ) + _emitter.emit_agent( + agent_name=agent_name, + model_deployment_name=model, + usage=agent_usage, + process_id=self.process_id, + user_id=self.user_id, + call_count=str(usage_dict["call_count"]), ) # Per-model events - for model_name, usage in summary["by_model"].items(): - track_event_if_configured( - "LLM_Model_Token_Usage", - { - "process_id": self.process_id, - "user_id": self.user_id, - "model_deployment_name": model_name, - "input_tokens": str(usage["input_tokens"]), - "output_tokens": str(usage["output_tokens"]), - "total_tokens": str(usage["total_tokens"]), - "call_count": str(usage["call_count"]), - }, + for model_name, usage_dict in summary["by_model"].items(): + model_usage = TokenUsage( + input_tokens=usage_dict["input_tokens"], + output_tokens=usage_dict["output_tokens"], + total_tokens=usage_dict["total_tokens"], + ) + _emitter.emit_model( + model_deployment_name=model_name, + usage=model_usage, + process_id=self.process_id, + user_id=self.user_id, + call_count=str(usage_dict["call_count"]), ) # Per-step (team) events - for step_name, usage in summary["by_step"].items(): - track_event_if_configured( - "LLM_Step_Token_Usage", - { - "process_id": self.process_id, - "user_id": self.user_id, - "step_name": step_name, - "input_tokens": str(usage["input_tokens"]), - "output_tokens": str(usage["output_tokens"]), - "total_tokens": str(usage["total_tokens"]), - "call_count": str(usage["call_count"]), - }, + for step_name, usage_dict in summary["by_step"].items(): + step_usage = TokenUsage( + input_tokens=usage_dict["input_tokens"], + output_tokens=usage_dict["output_tokens"], + total_tokens=usage_dict["total_tokens"], + ) + _emitter.emit_team( + team_name=step_name, + usage=step_usage, + process_id=self.process_id, + user_id=self.user_id, + call_count=str(usage_dict["call_count"]), ) logger.info( @@ -259,114 +269,29 @@ def emit_summary_events(self) -> None: def extract_usage_from_response(response: Any) -> TokenUsageRecord | None: """Extract token usage from an agent_framework or OpenAI SDK response object. - Handles multiple response shapes: - 1. response.usage (OpenAI SDK ChatCompletion) - 2. response.usage_details (agent_framework Content objects) - 3. response dict with usage keys - 4. AgentResponseUpdate with contents containing usage + Delegates to ``llm_token_telemetry.extract_usage()`` and converts the + result to a ``TokenUsageRecord`` for backward compatibility. """ - if response is None: + usage = extract_usage(response) + if usage is None: return None - - # 1. Direct .usage attribute (OpenAI ChatCompletion, Responses API) - usage = getattr(response, "usage", None) - if usage is not None: - record = _parse_usage_object(usage) - if record: - return record - - # 2. .usage_details or .details attribute - usage_details = getattr(response, "details", None) or getattr(response, "usage_details", None) - if usage_details is not None: - record = _parse_usage_object(usage_details) - if record: - return record - - # 3. raw_representation with usage - raw = getattr(response, "raw_representation", None) - if raw is not None: - raw_usage = getattr(raw, "usage", None) - if raw_usage is not None: - record = _parse_usage_object(raw_usage) - if record: - return record - if isinstance(raw, dict) and "usage" in raw: - record = _parse_usage_object(raw["usage"]) - if record: - return record - - # 4. contents list with usage items (AgentResponseUpdate) - contents = getattr(response, "contents", None) - if contents: - for item in contents: - # Try usage-typed content items first, then any item with details - ud = None - if getattr(item, "type", None) == "usage": - ud = getattr(item, "details", None) or getattr(item, "usage_details", None) - if ud is None: - ud = getattr(item, "details", None) or getattr(item, "usage_details", None) - if ud is not None: - record = _parse_usage_object(ud) - if record: - return record - # Dict content item - if isinstance(item, dict): - for key in ("details", "usage_details"): - if key in item: - record = _parse_usage_object(item[key]) - if record: - return record - if "input_token_count" in item or "total_token_count" in item: - record = _parse_usage_object(item) - if record: - return record - - # 5. additional_properties - addl = getattr(response, "additional_properties", None) - if isinstance(addl, dict) and "usage" in addl: - record = _parse_usage_object(addl["usage"]) - if record: - return record - - # 6. Dict response - if isinstance(response, dict): - if "usage" in response: - record = _parse_usage_object(response["usage"]) - if record: - return record - record = _parse_usage_object(response) - if record: - return record - - return None - - -def _get_field(obj: Any, *names: str) -> int: - """Read the first non-zero value from *obj* for the given field names. - - Works uniformly for dicts (via ``get``) and objects (via ``getattr``). - """ - getter = obj.get if isinstance(obj, dict) else lambda k, d=0: getattr(obj, k, d) - for name in names: - val = getter(name, 0) - if val: - return int(val) - return 0 + return TokenUsageRecord( + input_tokens=usage.input_tokens, + output_tokens=usage.output_tokens, + total_tokens=usage.total_tokens, + ) def _parse_usage_object(usage: Any) -> TokenUsageRecord | None: - """Parse a usage object (dict or object with attrs) into a TokenUsageRecord.""" - if usage is None: - return None - - inp = _get_field(usage, "input_token_count", "prompt_tokens", "input_tokens") - out = _get_field(usage, "output_token_count", "completion_tokens", "output_tokens") - tot = _get_field(usage, "total_token_count", "total_tokens") or (inp + out) + """Parse a usage object (dict or object with attrs) into a TokenUsageRecord. - if tot > 0 or inp > 0 or out > 0: - return TokenUsageRecord( - input_tokens=inp, - output_tokens=out, - total_tokens=tot if tot > 0 else inp + out, - ) - return None + Delegates to ``llm_token_telemetry.extract_usage_from_dict()``. + """ + result = extract_usage_from_dict(usage) + if result is None: + return None + return TokenUsageRecord( + input_tokens=result.input_tokens, + output_tokens=result.output_tokens, + total_tokens=result.total_tokens, + ) From 73ef8a080626c93d086209e1b1ab5b05273fe476 Mon Sep 17 00:00:00 2001 From: Priyanka-Microsoft Date: Mon, 1 Jun 2026 13:50:53 +0530 Subject: [PATCH 10/50] refactor: update llm_token_telemetry.py to latest cross-accelerator version and remove deployment/workbook changes - Updated llm_token_telemetry.py (processor + backend-api) to match Ajit's latest from customer-chatbot PR #236 (adds perf counters, emit timing, slow-emit warnings, batch overhead tracking) - Removed workbook/dashboard files (deploy-workbooks.ps1, KQL queries, workbook JSON files, tokenUsageWorkbook.bicep) - Reverted infra changes (main.bicep, main_custom.bicep, main.parameters.json) - Reverted Dockerfile changes (frontend, processor) - Removed test file (test_llm_token_telemetry.py) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- infra/dashboards/deploy-workbooks.ps1 | 86 --- infra/dashboards/token-usage-queries.kql | 113 ---- infra/dashboards/workbook-eks-content.json | 1 - infra/dashboards/workbook-gke-content.json | 1 - infra/main.bicep | 12 +- infra/main.parameters.json | 23 +- infra/main_custom.bicep | 12 +- infra/modules/tokenUsageWorkbook.bicep | 458 -------------- .../app/libs/logging/llm_token_telemetry.py | 156 ++++- src/frontend/Dockerfile | 1 - src/processor/Dockerfile | 3 +- .../src/tests/test_llm_token_telemetry.py | 572 ------------------ .../src/utils/llm_token_telemetry.py | 156 ++++- 13 files changed, 265 insertions(+), 1329 deletions(-) delete mode 100644 infra/dashboards/deploy-workbooks.ps1 delete mode 100644 infra/dashboards/token-usage-queries.kql delete mode 100644 infra/dashboards/workbook-eks-content.json delete mode 100644 infra/dashboards/workbook-gke-content.json delete mode 100644 infra/modules/tokenUsageWorkbook.bicep delete mode 100644 src/processor/src/tests/test_llm_token_telemetry.py diff --git a/infra/dashboards/deploy-workbooks.ps1 b/infra/dashboards/deploy-workbooks.ps1 deleted file mode 100644 index caa88198..00000000 --- a/infra/dashboards/deploy-workbooks.ps1 +++ /dev/null @@ -1,86 +0,0 @@ -# ============================================================= -# LLM Token Usage Workbook Deployment Script -# ============================================================= -# Usage: -# .\deploy-workbooks.ps1 -ResourceGroup -AppInsightsResourceId [-Location ] -# -# Example: -# .\deploy-workbooks.ps1 ` -# -ResourceGroup "rg-my-permanent-rg" ` -# -AppInsightsResourceId "/subscriptions//resourcegroups//providers/microsoft.insights/components/" ` -# -Location "australiaeast" -# ============================================================= - -param( - [Parameter(Mandatory=$true)] - [string]$ResourceGroup, - - [Parameter(Mandatory=$true)] - [string]$AppInsightsResourceId, - - [string]$Location = "australiaeast" -) - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path - -# Deploy GKE workbook -$gkeContent = Get-Content "$scriptDir\workbook-gke-content.json" -Raw -$gkeId = [guid]::NewGuid().ToString() - -$body = @{ - location = $Location - kind = "shared" - properties = @{ - displayName = "LLM Token Usage Dashboard - GKE" - serializedData = $gkeContent - version = "Notebook/1.0" - sourceId = $AppInsightsResourceId - category = "workbook" - } - tags = @{ - "hidden-title" = "LLM Token Usage Dashboard - GKE" - } -} | ConvertTo-Json -Depth 5 - -$bodyFile = [System.IO.Path]::GetTempFileName() -$body | Set-Content $bodyFile -Encoding UTF8 - -az rest --method PUT ` - --url "https://management.azure.com/subscriptions/$(az account show --query id -o tsv)/resourceGroups/$ResourceGroup/providers/microsoft.insights/workbooks/$($gkeId)?api-version=2022-04-01" ` - --body "@$bodyFile" ` - --headers "Content-Type=application/json" 2>&1 | Out-Null - -Write-Host "Deployed GKE workbook: $gkeId" -Remove-Item $bodyFile - -# Deploy EKS workbook -$eksContent = Get-Content "$scriptDir\workbook-eks-content.json" -Raw -$eksId = [guid]::NewGuid().ToString() - -$body = @{ - location = $Location - kind = "shared" - properties = @{ - displayName = "LLM Token Usage Dashboard - EKS" - serializedData = $eksContent - version = "Notebook/1.0" - sourceId = $AppInsightsResourceId - category = "workbook" - } - tags = @{ - "hidden-title" = "LLM Token Usage Dashboard - EKS" - } -} | ConvertTo-Json -Depth 5 - -$bodyFile = [System.IO.Path]::GetTempFileName() -$body | Set-Content $bodyFile -Encoding UTF8 - -az rest --method PUT ` - --url "https://management.azure.com/subscriptions/$(az account show --query id -o tsv)/resourceGroups/$ResourceGroup/providers/microsoft.insights/workbooks/$($eksId)?api-version=2022-04-01" ` - --body "@$bodyFile" ` - --headers "Content-Type=application/json" 2>&1 | Out-Null - -Write-Host "Deployed EKS workbook: $eksId" -Remove-Item $bodyFile - -Write-Host "`nDone! Both workbooks deployed to $ResourceGroup" diff --git a/infra/dashboards/token-usage-queries.kql b/infra/dashboards/token-usage-queries.kql deleted file mode 100644 index 38a0fc19..00000000 --- a/infra/dashboards/token-usage-queries.kql +++ /dev/null @@ -1,113 +0,0 @@ -// ============================================================================= -// LLM Token Usage Dashboard Queries for Application Insights -// ============================================================================= -// These KQL queries can be used in Azure Application Insights / Log Analytics -// to visualize token usage across agents, models, steps, and users. -// ============================================================================= - -// ---- 1. Overall Token Usage Summary (last 24h) ---- -customEvents -| where name == "LLM_Token_Usage_Summary" -| where timestamp > ago(24h) -| extend process_id = tostring(customDimensions.process_id), - total_input = toint(customDimensions.total_input_tokens), - total_output = toint(customDimensions.total_output_tokens), - total = toint(customDimensions.total_tokens), - call_count = toint(customDimensions.total_calls) -| project timestamp, process_id, total_input, total_output, total, call_count -| order by timestamp desc - -// ---- 2. Per-Agent Token Usage ---- -customEvents -| where name == "LLM_Agent_Token_Usage" -| where timestamp > ago(24h) -| extend agent_name = tostring(customDimensions.agent_name), - input_tokens = toint(customDimensions.input_tokens), - output_tokens = toint(customDimensions.output_tokens), - total_tokens = toint(customDimensions.total_tokens), - calls = toint(customDimensions.call_count), - process_id = tostring(customDimensions.process_id) -| summarize total_input = sum(input_tokens), - total_output = sum(output_tokens), - total = sum(total_tokens), - total_calls = sum(calls) - by agent_name -| order by total desc - -// ---- 3. Per-Model Token Usage ---- -customEvents -| where name == "LLM_Model_Token_Usage" -| where timestamp > ago(24h) -| extend model_name = tostring(customDimensions.model_deployment_name), - input_tokens = toint(customDimensions.input_tokens), - output_tokens = toint(customDimensions.output_tokens), - total_tokens = toint(customDimensions.total_tokens), - calls = toint(customDimensions.call_count), - process_id = tostring(customDimensions.process_id) -| summarize total_input = sum(input_tokens), - total_output = sum(output_tokens), - total = sum(total_tokens), - total_calls = sum(calls) - by model_name -| order by total desc - -// ---- 4. Per-Step (Team) Token Usage ---- -customEvents -| where name == "LLM_Step_Token_Usage" -| where timestamp > ago(24h) -| extend step_name = tostring(customDimensions.step_name), - input_tokens = toint(customDimensions.input_tokens), - output_tokens = toint(customDimensions.output_tokens), - total_tokens = toint(customDimensions.total_tokens), - calls = toint(customDimensions.call_count), - process_id = tostring(customDimensions.process_id) -| summarize total_input = sum(input_tokens), - total_output = sum(output_tokens), - total = sum(total_tokens), - total_calls = sum(calls) - by step_name -| order by total desc - -// ---- 5. Per-User Token Usage (requires user_id in process telemetry) ---- -customEvents -| where name == "LLM_Token_Usage_Summary" -| where timestamp > ago(24h) -| extend process_id = tostring(customDimensions.process_id), - total_tokens = toint(customDimensions.total_tokens), - user_id = tostring(customDimensions.user_id) -| summarize total = sum(total_tokens), runs = count() by user_id -| order by total desc - -// ---- 6. Individual LLM Call Log ---- -customEvents -| where name == "LLM_Token_Usage" -| where timestamp > ago(24h) -| extend agent_name = tostring(customDimensions.agent_name), - step_name = tostring(customDimensions.step_name), - model = tostring(customDimensions.model_deployment_name), - input_tokens = toint(customDimensions.input_tokens), - output_tokens = toint(customDimensions.output_tokens), - total_tokens = toint(customDimensions.total_tokens), - process_id = tostring(customDimensions.process_id) -| project timestamp, process_id, agent_name, step_name, model, input_tokens, output_tokens, total_tokens -| order by timestamp desc - -// ---- 7. Hourly Token Usage Trend ---- -customEvents -| where name == "LLM_Token_Usage" -| where timestamp > ago(7d) -| extend total_tokens = toint(customDimensions.total_tokens) -| summarize hourly_tokens = sum(total_tokens), calls = count() by bin(timestamp, 1h) -| order by timestamp asc -| render timechart - -// ---- 8. Estimated Cost (GPT-4o pricing: $2.50/1M input, $10/1M output) ---- -customEvents -| where name == "LLM_Token_Usage_Summary" -| where timestamp > ago(24h) -| extend process_id = tostring(customDimensions.process_id), - input_tokens = toint(customDimensions.total_input_tokens), - output_tokens = toint(customDimensions.total_output_tokens) -| extend estimated_cost_usd = (input_tokens / 1000000.0 * 2.50) + (output_tokens / 1000000.0 * 10.0) -| project timestamp, process_id, input_tokens, output_tokens, estimated_cost_usd -| order by estimated_cost_usd desc diff --git a/infra/dashboards/workbook-eks-content.json b/infra/dashboards/workbook-eks-content.json deleted file mode 100644 index 04433e99..00000000 --- a/infra/dashboards/workbook-eks-content.json +++ /dev/null @@ -1 +0,0 @@ -{"version":"Notebook/1.0","items":[{"type":1,"content":{"json":"# LLM Token Usage Dashboard\n\nThis workbook provides comprehensive visibility into LLM token consumption across agents, models, workflow steps, and users.\n\n---"},"name":"header"},{"type":9,"content":{"version":"KqlParameterItem/1.0","parameters":[{"id":"time-range-param","version":"KqlParameterItem/1.0","name":"TimeRange","type":4,"isRequired":true,"value":{"durationMs":1800000,"endTime":"2026-05-21T06:50:00.000Z"},"typeSettings":{"selectableValues":[{"durationMs":3600000},{"durationMs":14400000},{"durationMs":86400000},{"durationMs":259200000},{"durationMs":604800000},{"durationMs":2592000000}],"allowCustom":true},"label":"Time Range"}],"style":"pills","queryType":0,"resourceType":"microsoft.insights/components"},"name":"parameters"},{"type":1,"content":{"json":"## Overall Token Usage Summary"},"name":"summary-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| summarize \n total_input = sum(toint(customDimensions.total_input_tokens)),\n total_output = sum(toint(customDimensions.total_output_tokens)),\n total = sum(toint(customDimensions.total_tokens)),\n total_calls = sum(toint(customDimensions.total_calls)),\n processes = dcount(tostring(customDimensions.process_id))","size":4,"title":"Token Usage Totals","queryType":0,"resourceType":"microsoft.insights/components","visualization":"tiles","tileSettings":{"titleContent":{"columnMatch":"Column1","formatter":1},"leftContent":{"columnMatch":"total","formatter":12,"formatOptions":{"palette":"auto"},"numberFormat":{"unit":0,"options":{"style":"decimal","maximumFractionDigits":0}}},"showBorder":true}},"name":"summary-tiles"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_input = toint(customDimensions.total_input_tokens),\n total_output = toint(customDimensions.total_output_tokens),\n total = toint(customDimensions.total_tokens),\n call_count = toint(customDimensions.total_calls)\n| project timestamp, process_id, total_input, total_output, total, call_count\n| order by timestamp desc","size":0,"title":"Token Usage by Process","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"blue"}}]}},"name":"summary-table"},{"type":1,"content":{"json":"## Per-Agent Token Usage"},"name":"agent-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Agent_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by agent_name\n| order by total desc","size":0,"title":"Token Consumption by Agent","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"blue"}}]}},"customWidth":"50","name":"agent-table"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Agent_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by agent_name\n| order by total desc","size":0,"title":"Token Distribution by Agent","queryType":0,"resourceType":"microsoft.insights/components","visualization":"piechart"},"customWidth":"50","name":"agent-chart"},{"type":1,"content":{"json":"## Per-Model Token Usage"},"name":"model-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Model_Token_Usage\"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by model_name\n| order by total desc","size":0,"title":"Token Consumption by Model","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"green"}}]}},"customWidth":"50","name":"model-table"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Model_Token_Usage\"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by model_name\n| order by total desc","size":0,"title":"Token Distribution by Model","queryType":0,"resourceType":"microsoft.insights/components","visualization":"piechart"},"customWidth":"50","name":"model-chart"},{"type":1,"content":{"json":"## Per-Step (Team) Token Usage"},"name":"step-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Step_Token_Usage\"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc","size":0,"title":"Token Consumption by Workflow Step","queryType":0,"resourceType":"microsoft.insights/components","visualization":"barchart","chartSettings":{"xAxis":"step_name","yAxis":"total","group":"step_name"}},"customWidth":"50","name":"step-chart"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Step_Token_Usage\"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc","size":0,"title":"Step Usage Details","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"orange"}}]}},"customWidth":"50","name":"step-table"},{"type":1,"content":{"json":"## Per-User Token Usage"},"name":"user-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_tokens = toint(customDimensions.total_tokens),\n user_id = tostring(customDimensions.user_id)\n| summarize total = sum(total_tokens), runs = count() by user_id\n| order by total desc","size":0,"title":"Token Usage by User","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"purple"}}]}},"name":"user-table"},{"type":1,"content":{"json":"## Token Usage Trends"},"name":"trend-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage\"\n| where timestamp {TimeRange}\n| extend total_tokens = toint(customDimensions.total_tokens)\n| summarize hourly_tokens = sum(total_tokens), calls = count() by bin(timestamp, 1h)\n| order by timestamp asc","size":0,"title":"Hourly Token Consumption","queryType":0,"resourceType":"microsoft.insights/components","visualization":"linechart","chartSettings":{"xAxis":"timestamp","yAxis":"hourly_tokens","showLegend":true}},"name":"trend-chart"},{"type":1,"content":{"json":"## Estimated Cost\n\n> Cost estimates use GPT-4o pricing: **$2.50 / 1M input tokens**, **$10.00 / 1M output tokens**. Adjust for your actual model pricing."},"name":"cost-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n input_tokens = toint(customDimensions.total_input_tokens),\n output_tokens = toint(customDimensions.total_output_tokens)\n| extend estimated_cost_usd = round((input_tokens / 1000000.0 * 2.50) + (output_tokens / 1000000.0 * 10.0), 4)\n| project timestamp, process_id, input_tokens, output_tokens, estimated_cost_usd\n| order by estimated_cost_usd desc","size":0,"title":"Estimated Cost per Process (USD)","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"estimated_cost_usd","formatter":3,"formatOptions":{"palette":"redBright"}}]}},"name":"cost-table"},{"type":1,"content":{"json":"## Individual LLM Call Log"},"name":"calls-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n step_name = tostring(customDimensions.step_name),\n model = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n process_id = tostring(customDimensions.process_id)\n| project timestamp, process_id, agent_name, step_name, model, input_tokens, output_tokens, total_tokens\n| order by timestamp desc\n| take 200","size":0,"title":"Recent LLM Calls (last 200)","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total_tokens","formatter":3,"formatOptions":{"palette":"blue"}}]}},"name":"calls-table"}],"isLocked":false,"fallbackResourceIds":["/subscriptions/1d5876cd-7603-407a-96d2-ae5ca9a9c5f3/resourcegroups/rg-pricmglogp33/providers/microsoft.insights/components/appi-pricmglogp33usmqm"]} diff --git a/infra/dashboards/workbook-gke-content.json b/infra/dashboards/workbook-gke-content.json deleted file mode 100644 index ad05834c..00000000 --- a/infra/dashboards/workbook-gke-content.json +++ /dev/null @@ -1 +0,0 @@ -{"version":"Notebook/1.0","items":[{"type":1,"content":{"json":"# LLM Token Usage Dashboard\n\nThis workbook provides comprehensive visibility into LLM token consumption across agents, models, workflow steps, and users.\n\n---"},"name":"header"},{"type":9,"content":{"version":"KqlParameterItem/1.0","parameters":[{"id":"time-range-param","version":"KqlParameterItem/1.0","name":"TimeRange","type":4,"isRequired":true,"value":{"durationMs":1500000,"endTime":"2026-05-21T06:08:00.000Z"},"typeSettings":{"selectableValues":[{"durationMs":3600000},{"durationMs":14400000},{"durationMs":86400000},{"durationMs":259200000},{"durationMs":604800000},{"durationMs":2592000000}],"allowCustom":true},"label":"Time Range"}],"style":"pills","queryType":0,"resourceType":"microsoft.insights/components"},"name":"parameters"},{"type":1,"content":{"json":"## Overall Token Usage Summary"},"name":"summary-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| summarize \n total_input = sum(toint(customDimensions.total_input_tokens)),\n total_output = sum(toint(customDimensions.total_output_tokens)),\n total = sum(toint(customDimensions.total_tokens)),\n total_calls = sum(toint(customDimensions.total_calls)),\n processes = dcount(tostring(customDimensions.process_id))","size":4,"title":"Token Usage Totals","queryType":0,"resourceType":"microsoft.insights/components","visualization":"tiles","tileSettings":{"titleContent":{"columnMatch":"Column1","formatter":1},"leftContent":{"columnMatch":"total","formatter":12,"formatOptions":{"palette":"auto"},"numberFormat":{"unit":0,"options":{"style":"decimal","maximumFractionDigits":0}}},"showBorder":true}},"name":"summary-tiles"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_input = toint(customDimensions.total_input_tokens),\n total_output = toint(customDimensions.total_output_tokens),\n total = toint(customDimensions.total_tokens),\n call_count = toint(customDimensions.total_calls)\n| project timestamp, process_id, total_input, total_output, total, call_count\n| order by timestamp desc","size":0,"title":"Token Usage by Process","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"blue"}}]}},"name":"summary-table"},{"type":1,"content":{"json":"## Per-Agent Token Usage"},"name":"agent-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Agent_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by agent_name\n| order by total desc","size":0,"title":"Token Consumption by Agent","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"blue"}}]}},"customWidth":"50","name":"agent-table"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Agent_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by agent_name\n| order by total desc","size":0,"title":"Token Distribution by Agent","queryType":0,"resourceType":"microsoft.insights/components","visualization":"piechart"},"customWidth":"50","name":"agent-chart"},{"type":1,"content":{"json":"## Per-Model Token Usage"},"name":"model-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Model_Token_Usage\"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by model_name\n| order by total desc","size":0,"title":"Token Consumption by Model","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"green"}}]}},"customWidth":"50","name":"model-table"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Model_Token_Usage\"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by model_name\n| order by total desc","size":0,"title":"Token Distribution by Model","queryType":0,"resourceType":"microsoft.insights/components","visualization":"piechart"},"customWidth":"50","name":"model-chart"},{"type":1,"content":{"json":"## Per-Step (Team) Token Usage"},"name":"step-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Step_Token_Usage\"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc","size":0,"title":"Token Consumption by Workflow Step","queryType":0,"resourceType":"microsoft.insights/components","visualization":"barchart","chartSettings":{"xAxis":"step_name","yAxis":"total","group":"step_name"}},"customWidth":"50","name":"step-chart"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Step_Token_Usage\"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc","size":0,"title":"Step Usage Details","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"orange"}}]}},"customWidth":"50","name":"step-table"},{"type":1,"content":{"json":"## Per-User Token Usage"},"name":"user-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_tokens = toint(customDimensions.total_tokens),\n user_id = tostring(customDimensions.user_id)\n| summarize total = sum(total_tokens), runs = count() by user_id\n| order by total desc","size":0,"title":"Token Usage by User","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total","formatter":3,"formatOptions":{"palette":"purple"}}]}},"name":"user-table"},{"type":1,"content":{"json":"## Token Usage Trends"},"name":"trend-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage\"\n| where timestamp {TimeRange}\n| extend total_tokens = toint(customDimensions.total_tokens)\n| summarize hourly_tokens = sum(total_tokens), calls = count() by bin(timestamp, 1h)\n| order by timestamp asc","size":0,"title":"Hourly Token Consumption","queryType":0,"resourceType":"microsoft.insights/components","visualization":"linechart","chartSettings":{"xAxis":"timestamp","yAxis":"hourly_tokens","showLegend":true}},"name":"trend-chart"},{"type":1,"content":{"json":"## Estimated Cost\n\n> Cost estimates use GPT-4o pricing: **$2.50 / 1M input tokens**, **$10.00 / 1M output tokens**. Adjust for your actual model pricing."},"name":"cost-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage_Summary\"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n input_tokens = toint(customDimensions.total_input_tokens),\n output_tokens = toint(customDimensions.total_output_tokens)\n| extend estimated_cost_usd = round((input_tokens / 1000000.0 * 2.50) + (output_tokens / 1000000.0 * 10.0), 4)\n| project timestamp, process_id, input_tokens, output_tokens, estimated_cost_usd\n| order by estimated_cost_usd desc","size":0,"title":"Estimated Cost per Process (USD)","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"estimated_cost_usd","formatter":3,"formatOptions":{"palette":"redBright"}}]}},"name":"cost-table"},{"type":1,"content":{"json":"## Individual LLM Call Log"},"name":"calls-header"},{"type":3,"content":{"version":"KqlItem/1.0","query":"customEvents\n| where name == \"LLM_Token_Usage\"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n step_name = tostring(customDimensions.step_name),\n model = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n process_id = tostring(customDimensions.process_id)\n| project timestamp, process_id, agent_name, step_name, model, input_tokens, output_tokens, total_tokens\n| order by timestamp desc\n| take 200","size":0,"title":"Recent LLM Calls (last 200)","queryType":0,"resourceType":"microsoft.insights/components","visualization":"table","gridSettings":{"formatters":[{"columnMatch":"total_tokens","formatter":3,"formatOptions":{"palette":"blue"}}]}},"name":"calls-table"}],"isLocked":false,"fallbackResourceIds":["/subscriptions/1d5876cd-7603-407a-96d2-ae5ca9a9c5f3/resourcegroups/rg-pricmglogp33/providers/microsoft.insights/components/appi-pricmglogp33usmqm"]} diff --git a/infra/main.bicep b/infra/main.bicep index 3306c57a..904fee03 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -91,7 +91,7 @@ param enableTelemetry bool = true param enablePrivateNetworking bool = false @description('Optional. Enable monitoring applicable resources, aligned with the Well Architected Framework recommendations. This setting enables Application Insights and Log Analytics and configures all the resources applicable resources to send logs. Defaults to false.') -param enableMonitoring bool = true +param enableMonitoring bool = false @description('Optional. Enable scalability for applicable resources, aligned with the Well Architected Framework recommendations. Defaults to false.') param enableScalability bool = false @@ -310,16 +310,6 @@ module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (en } } -// ========== LLM Token Usage Workbook ========== // -module tokenUsageWorkbook './modules/tokenUsageWorkbook.bicep' = if (enableMonitoring) { - name: take('module.token-usage-workbook.${solutionSuffix}', 64) - params: { - location: solutionLocation - applicationInsightsResourceId: applicationInsights!.outputs.resourceId - tags: allTags - } -} - // ========== Virtual Network ========== // module virtualNetwork './modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { name: take('module.virtual-network.${solutionSuffix}', 64) diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 6df293a4..b4a1a7cc 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -26,17 +26,11 @@ "gptDeploymentCapacity": { "value": "${AZURE_ENV_GPT_MODEL_CAPACITY}" }, - "enableTelemetry": { - "value": true - }, - "enableMonitoring": { - "value": true - }, - "enablePrivateNetworking": { - "value": false + "existingLogAnalyticsWorkspaceId": { + "value": "${AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID}" }, - "enableScalability": { - "value": false + "existingFoundryProjectResourceId": { + "value": "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}" }, "vmAdminUsername": { "value": "${AZURE_ENV_VM_ADMIN_USERNAME}" @@ -44,17 +38,8 @@ "vmAdminPassword": { "value": "${AZURE_ENV_VM_ADMIN_PASSWORD}" }, - "existingLogAnalyticsWorkspaceId": { - "value": "${AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID}" - }, - "existingFoundryProjectResourceId": { - "value": "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}" - }, "imageTag": { "value": "${AZURE_ENV_IMAGE_TAG}" - }, - "vmSize": { - "value": "${AZURE_ENV_VM_SIZE}" } } } diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 9e106e59..f93b93ab 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -84,7 +84,7 @@ param enableTelemetry bool = true param enablePrivateNetworking bool = false @description('Optional. Enable monitoring applicable resources, aligned with the Well Architected Framework recommendations. This setting enables Application Insights and Log Analytics and configures all the resources applicable resources to send logs. Defaults to false.') -param enableMonitoring bool = true +param enableMonitoring bool = false @description('Optional. Enable scalability for applicable resources, aligned with the Well Architected Framework recommendations. Defaults to false.') param enableScalability bool = false @@ -288,16 +288,6 @@ module applicationInsights 'br/public:avm/res/insights/component:0.6.0' = if (en } } -// ========== LLM Token Usage Workbook ========== // -module tokenUsageWorkbook './modules/tokenUsageWorkbook.bicep' = if (enableMonitoring) { - name: take('module.token-usage-workbook.${solutionSuffix}', 64) - params: { - location: solutionLocation - applicationInsightsResourceId: applicationInsights!.outputs.resourceId - tags: allTags - } -} - // ========== Virtual Network ========== // module virtualNetwork './modules/virtualNetwork.bicep' = if (enablePrivateNetworking) { name: take('module.virtual-network.${solutionSuffix}', 64) diff --git a/infra/modules/tokenUsageWorkbook.bicep b/infra/modules/tokenUsageWorkbook.bicep deleted file mode 100644 index 6531bdda..00000000 --- a/infra/modules/tokenUsageWorkbook.bicep +++ /dev/null @@ -1,458 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -@description('Required. The location for the workbook resource.') -param location string - -@description('Required. The resource ID of the Application Insights instance to query.') -param applicationInsightsResourceId string - -@description('Optional. Tags to apply to the workbook resource.') -param tags object = {} - -@description('Optional. Display name for the workbook.') -param workbookDisplayName string = 'LLM Token Usage Dashboard' - -// Generate a deterministic GUID for the workbook based on resource group and name -var workbookId = guid(resourceGroup().id, 'token-usage-workbook') - -var workbookContent = { - version: 'Notebook/1.0' - items: [ - { - type: 1 - content: { - json: '# LLM Token Usage Dashboard\n\nThis workbook provides comprehensive visibility into LLM token consumption across agents, models, workflow steps, and users.\n\n---' - } - name: 'header' - } - { - type: 9 - content: { - version: 'KqlParameterItem/1.0' - parameters: [ - { - id: 'time-range-param' - version: 'KqlParameterItem/1.0' - name: 'TimeRange' - type: 4 - isRequired: true - value: { - durationMs: 86400000 - } - typeSettings: { - selectableValues: [ - { durationMs: 3600000 } - { durationMs: 14400000 } - { durationMs: 86400000 } - { durationMs: 259200000 } - { durationMs: 604800000 } - { durationMs: 2592000000 } - ] - allowCustom: true - } - label: 'Time Range' - } - ] - style: 'pills' - queryType: 0 - resourceType: 'microsoft.insights/components' - } - name: 'parameters' - } - // ===== Row 1: Summary Tiles ===== - { - type: 1 - content: { - json: '## Overall Token Usage Summary' - } - name: 'summary-header' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Token_Usage_Summary"\n| where timestamp {TimeRange}\n| summarize \n total_input = sum(toint(customDimensions.total_input_tokens)),\n total_output = sum(toint(customDimensions.total_output_tokens)),\n total = sum(toint(customDimensions.total_tokens)),\n total_calls = sum(toint(customDimensions.total_calls)),\n processes = dcount(tostring(customDimensions.process_id))' - size: 4 - title: 'Token Usage Totals' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'tiles' - tileSettings: { - titleContent: { - columnMatch: 'Column1' - formatter: 1 - } - leftContent: { - columnMatch: 'total' - formatter: 12 - formatOptions: { - palette: 'auto' - } - numberFormat: { - unit: 0 - options: { - style: 'decimal' - maximumFractionDigits: 0 - } - } - } - showBorder: true - } - } - name: 'summary-tiles' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Token_Usage_Summary"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_input = toint(customDimensions.total_input_tokens),\n total_output = toint(customDimensions.total_output_tokens),\n total = toint(customDimensions.total_tokens),\n call_count = toint(customDimensions.total_calls)\n| project timestamp, process_id, total_input, total_output, total, call_count\n| order by timestamp desc' - size: 0 - title: 'Token Usage by Process' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'table' - gridSettings: { - formatters: [ - { - columnMatch: 'total' - formatter: 3 - formatOptions: { - palette: 'blue' - } - } - ] - } - } - name: 'summary-table' - } - // ===== Row 2: Per-Agent Token Usage ===== - { - type: 1 - content: { - json: '## Per-Agent Token Usage' - } - name: 'agent-header' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Agent_Token_Usage"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by agent_name\n| order by total desc' - size: 0 - title: 'Token Consumption by Agent' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'table' - gridSettings: { - formatters: [ - { - columnMatch: 'total' - formatter: 3 - formatOptions: { - palette: 'blue' - } - } - ] - } - } - customWidth: '50' - name: 'agent-table' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Agent_Token_Usage"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by agent_name\n| order by total desc' - size: 0 - title: 'Token Distribution by Agent' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'piechart' - } - customWidth: '50' - name: 'agent-chart' - } - // ===== Row 3: Per-Model Token Usage ===== - { - type: 1 - content: { - json: '## Per-Model Token Usage' - } - name: 'model-header' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Model_Token_Usage"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by model_name\n| order by total desc' - size: 0 - title: 'Token Consumption by Model' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'table' - gridSettings: { - formatters: [ - { - columnMatch: 'total' - formatter: 3 - formatOptions: { - palette: 'green' - } - } - ] - } - } - customWidth: '50' - name: 'model-table' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Model_Token_Usage"\n| where timestamp {TimeRange}\n| extend model_name = tostring(customDimensions.model_deployment_name),\n total_tokens = toint(customDimensions.total_tokens)\n| summarize total = sum(total_tokens) by model_name\n| order by total desc' - size: 0 - title: 'Token Distribution by Model' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'piechart' - } - customWidth: '50' - name: 'model-chart' - } - // ===== Row 4: Per-Step (Team) Token Usage ===== - { - type: 1 - content: { - json: '## Per-Step (Team) Token Usage' - } - name: 'step-header' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Step_Token_Usage"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc' - size: 0 - title: 'Token Consumption by Workflow Step' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'barchart' - chartSettings: { - xAxis: 'step_name' - yAxis: 'total' - group: 'step_name' - } - } - customWidth: '50' - name: 'step-chart' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Step_Token_Usage"\n| where timestamp {TimeRange}\n| extend step_name = tostring(customDimensions.step_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n calls = toint(customDimensions.call_count)\n| summarize total_input = sum(input_tokens),\n total_output = sum(output_tokens),\n total = sum(total_tokens),\n total_calls = sum(calls)\n by step_name\n| order by total desc' - size: 0 - title: 'Step Usage Details' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'table' - gridSettings: { - formatters: [ - { - columnMatch: 'total' - formatter: 3 - formatOptions: { - palette: 'orange' - } - } - ] - } - } - customWidth: '50' - name: 'step-table' - } - // ===== Row 5: Per-User Token Usage ===== - { - type: 1 - content: { - json: '## Per-User Token Usage' - } - name: 'user-header' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Token_Usage_Summary"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n total_tokens = toint(customDimensions.total_tokens),\n user_id = tostring(customDimensions.user_id)\n| summarize total = sum(total_tokens), runs = count() by user_id\n| order by total desc' - size: 0 - title: 'Token Usage by User' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'table' - gridSettings: { - formatters: [ - { - columnMatch: 'total' - formatter: 3 - formatOptions: { - palette: 'purple' - } - } - ] - } - } - name: 'user-table' - } - // ===== Row 6: Hourly Token Usage Trend ===== - { - type: 1 - content: { - json: '## Token Usage Trends' - } - name: 'trend-header' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Token_Usage"\n| where timestamp {TimeRange}\n| extend total_tokens = toint(customDimensions.total_tokens)\n| summarize hourly_tokens = sum(total_tokens), calls = count() by bin(timestamp, 1h)\n| order by timestamp asc' - size: 0 - title: 'Hourly Token Consumption' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'linechart' - chartSettings: { - xAxis: 'timestamp' - yAxis: 'hourly_tokens' - showLegend: true - } - } - name: 'trend-chart' - } - // ===== Row 7: Estimated Cost ===== - { - type: 1 - content: { - json: '## Estimated Cost\n\n> Cost estimates use GPT-4o pricing: **$2.50 / 1M input tokens**, **$10.00 / 1M output tokens**. Adjust for your actual model pricing.' - } - name: 'cost-header' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Token_Usage_Summary"\n| where timestamp {TimeRange}\n| extend process_id = tostring(customDimensions.process_id),\n input_tokens = toint(customDimensions.total_input_tokens),\n output_tokens = toint(customDimensions.total_output_tokens)\n| extend estimated_cost_usd = round((input_tokens / 1000000.0 * 2.50) + (output_tokens / 1000000.0 * 10.0), 4)\n| project timestamp, process_id, input_tokens, output_tokens, estimated_cost_usd\n| order by estimated_cost_usd desc' - size: 0 - title: 'Estimated Cost per Process (USD)' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'table' - gridSettings: { - formatters: [ - { - columnMatch: 'estimated_cost_usd' - formatter: 3 - formatOptions: { - palette: 'redBright' - } - } - ] - } - } - name: 'cost-table' - } - // ===== Row 8: Individual LLM Call Log ===== - { - type: 1 - content: { - json: '## Individual LLM Call Log' - } - name: 'calls-header' - } - { - type: 3 - content: { - version: 'KqlItem/1.0' - query: 'customEvents\n| where name == "LLM_Token_Usage"\n| where timestamp {TimeRange}\n| extend agent_name = tostring(customDimensions.agent_name),\n step_name = tostring(customDimensions.step_name),\n model = tostring(customDimensions.model_deployment_name),\n input_tokens = toint(customDimensions.input_tokens),\n output_tokens = toint(customDimensions.output_tokens),\n total_tokens = toint(customDimensions.total_tokens),\n process_id = tostring(customDimensions.process_id)\n| project timestamp, process_id, agent_name, step_name, model, input_tokens, output_tokens, total_tokens\n| order by timestamp desc\n| take 200' - size: 0 - title: 'Recent LLM Calls (last 200)' - queryType: 0 - resourceType: 'microsoft.insights/components' - crossComponentResources: [ - applicationInsightsResourceId - ] - visualization: 'table' - gridSettings: { - formatters: [ - { - columnMatch: 'total_tokens' - formatter: 3 - formatOptions: { - palette: 'blue' - } - } - ] - } - } - name: 'calls-table' - } - ] - isLocked: false - fallbackResourceIds: [ - applicationInsightsResourceId - ] -} - -resource tokenUsageWorkbook 'Microsoft.Insights/workbooks@2023-06-01' = { - name: workbookId - location: location - tags: tags - kind: 'shared' - properties: { - displayName: workbookDisplayName - category: 'workbook' - version: '1.0' - serializedData: string(workbookContent) - sourceId: applicationInsightsResourceId - } -} - -@description('The resource ID of the created workbook.') -output resourceId string = tokenUsageWorkbook.id - -@description('The name of the created workbook.') -output name string = tokenUsageWorkbook.name diff --git a/src/backend-api/src/app/libs/logging/llm_token_telemetry.py b/src/backend-api/src/app/libs/logging/llm_token_telemetry.py index b3035fc8..91f670c5 100644 --- a/src/backend-api/src/app/libs/logging/llm_token_telemetry.py +++ b/src/backend-api/src/app/libs/logging/llm_token_telemetry.py @@ -53,6 +53,7 @@ import logging import os import random +import time from contextlib import AbstractContextManager from dataclasses import dataclass, field from typing import Any, Callable, Iterable, Mapping, Optional @@ -472,6 +473,16 @@ def __init__( k: ("" if v is None else str(v)) for k, v in raw_static.items() } + # Performance counters. ``perf_*`` accumulate wall-clock nanoseconds + # spent inside ``emit()`` so callers can verify telemetry overhead is + # negligible. ``perf_slow_emit_threshold_ms`` is the soft threshold + # above which a WARNING is logged for an individual emit (default + # 50 ms -- emits should normally take well under 1 ms). + self._perf_total_ns: int = 0 + self._perf_emit_count: int = 0 + self._perf_max_ns: int = 0 + self.perf_slow_emit_threshold_ms: float = 50.0 + # -- public surface --------------------------------------------------- @property def enabled(self) -> bool: @@ -539,28 +550,74 @@ def emit(self, event_name: str, **dimensions: Any) -> None: Non-string values are stringified. ``None`` values are dropped. Any ``user_id`` value is passed through the configured hasher. - Never raises. + Never raises. Wall-clock duration is recorded for performance audit + (see :meth:`perf_stats`). """ - props = dict(self._static) # cheap shallow copy of pre-stringified dims - for k, v in dimensions.items(): - if v is None: - continue - if k == "user_id": - v = self._apply_user_id_hash(v) - if v is None or v == "": + start_ns = time.perf_counter_ns() + try: + props = dict(self._static) # cheap shallow copy of pre-stringified dims + for k, v in dimensions.items(): + if v is None: continue - props[k] = v if isinstance(v, str) else str(v) + if k == "user_id": + v = self._apply_user_id_hash(v) + if v is None or v == "": + continue + props[k] = v if isinstance(v, str) else str(v) + + if not self.enabled: + self._log.debug( + "App Insights not configured -- skipping event %s (%s)", + event_name, props, + ) + return + try: + self._sink(event_name, props) # type: ignore[misc] + except Exception as exc: # never break the caller + self._log.warning("track_event(%s) failed: %s", event_name, exc) + finally: + elapsed_ns = time.perf_counter_ns() - start_ns + self._perf_total_ns += elapsed_ns + self._perf_emit_count += 1 + if elapsed_ns > self._perf_max_ns: + self._perf_max_ns = elapsed_ns + elapsed_ms = elapsed_ns / 1_000_000.0 + if elapsed_ms > self.perf_slow_emit_threshold_ms: + self._log.warning( + "Token telemetry emit slow: event=%s duration_ms=%.3f", + event_name, elapsed_ms, + ) + else: + self._log.debug( + "Token telemetry emit: event=%s duration_ms=%.3f", + event_name, elapsed_ms, + ) + + # -- performance audit ------------------------------------------------ + def perf_stats(self) -> dict[str, float]: + """Return cumulative telemetry-overhead stats since process start + (or since :meth:`reset_perf_stats`). + + Keys: + ``emit_count`` -- number of events emitted + ``total_ms`` -- total wall-clock time spent inside ``emit`` + ``avg_ms`` -- mean per-event duration + ``max_ms`` -- slowest single emit observed + """ + count = self._perf_emit_count + total_ms = self._perf_total_ns / 1_000_000.0 + return { + "emit_count": float(count), + "total_ms": total_ms, + "avg_ms": (total_ms / count) if count else 0.0, + "max_ms": self._perf_max_ns / 1_000_000.0, + } - if not self.enabled: - self._log.debug( - "App Insights not configured -- skipping event %s (%s)", - event_name, props, - ) - return - try: - self._sink(event_name, props) # type: ignore[misc] - except Exception as exc: # never break the caller - self._log.warning("track_event(%s) failed: %s", event_name, exc) + def reset_perf_stats(self) -> None: + """Zero the perf counters (useful for tests and load-tests).""" + self._perf_total_ns = 0 + self._perf_emit_count = 0 + self._perf_max_ns = 0 # -- typed convenience emitters -------------------------------------- def emit_agent( @@ -713,14 +770,12 @@ def emit_all( agents.update({k: v for k, v in additional_agents.items() if k}) models = {m for m in agents.values() if m} - self.emit_summary( - usage=usage, - agent_count=len(agents), - model_count=len(models) or 1, - primary_model=model_deployment_name, - additional_agents=additional_agents, - **dimensions, - ) + # Wall-clock timing of the whole emit_all path so callers (or tests) + # can verify the telemetry path stays cheap relative to the LLM call + # it instruments. + batch_start_ns = time.perf_counter_ns() + + # Defer summary until last so we can stamp the batch overhead on it. self.emit_agent( agent_name=agent_name, model_deployment_name=model_deployment_name, @@ -748,6 +803,17 @@ def emit_all( model_deployment_name=model_deployment_name, ) + batch_overhead_ms = (time.perf_counter_ns() - batch_start_ns) / 1_000_000.0 + self.emit_summary( + usage=usage, + agent_count=len(agents), + model_count=len(models) or 1, + primary_model=model_deployment_name, + additional_agents=additional_agents, + telemetry_overhead_ms=f"{batch_overhead_ms:.3f}", + **dimensions, + ) + self._log.info( "[TOKEN USAGE] agent=%s model=%s input=%d output=%d total=%d %s", agent_name, @@ -804,6 +870,12 @@ def __init__( self.emit_team_event = emit_team_event self.dimensions = dict(dimensions) self.usage = TokenUsage() + # Wall-clock nanoseconds spent inside extraction (``add*``) and the + # final ``__exit__`` emit, respectively. Surfaced for callers that + # want to verify the helper doesn't add measurable latency. Available + # as ``scope.extract_ms`` / ``scope.emit_ms`` after the scope closes. + self._extract_ns: int = 0 + self._emit_ns: int = 0 # -- accumulation ----------------------------------------------------- def add(self, source: Any) -> Optional[TokenUsage]: @@ -812,11 +884,14 @@ def add(self, source: Any) -> Optional[TokenUsage]: Never raises -- extraction failures return ``None`` and are logged at DEBUG. """ + start_ns = time.perf_counter_ns() try: found = extract_usage(source) or extract_usage_from_stream_chunk(source) except Exception as exc: # belt + braces; extractors are already safe logger.debug("TokenUsageScope.add failed: %s", exc, exc_info=True) return None + finally: + self._extract_ns += time.perf_counter_ns() - start_ns if found: self.usage = self.usage + found return found @@ -828,9 +903,26 @@ def add_chunks(self, chunks: Iterable[Any]) -> None: for c in chunks: self.add(c) + # -- timing properties ----------------------------------------------- + @property + def extract_ms(self) -> float: + """Total ms spent inside :meth:`add` / :meth:`add_chunks`.""" + return self._extract_ns / 1_000_000.0 + + @property + def emit_ms(self) -> float: + """Total ms spent in the on-exit emit batch.""" + return self._emit_ns / 1_000_000.0 + + @property + def total_overhead_ms(self) -> float: + """Total telemetry overhead added by this scope (extract + emit).""" + return self.extract_ms + self.emit_ms + # -- context manager -------------------------------------------------- def __exit__(self, exc_type, exc, tb) -> None: # Always emit (best-effort) regardless of exception status. + emit_start_ns = time.perf_counter_ns() try: self.emitter.emit_all( agent_name=self.agent_name, @@ -843,6 +935,16 @@ def __exit__(self, exc_type, exc, tb) -> None: ) except Exception as emit_exc: # pragma: no cover - belt + braces logger.warning("TokenUsageScope emit failed: %s", emit_exc) + finally: + self._emit_ns += time.perf_counter_ns() - emit_start_ns + logger.debug( + "TokenUsageScope overhead: agent=%s extract_ms=%.3f " + "emit_ms=%.3f total_ms=%.3f", + self.agent_name, + self.extract_ms, + self.emit_ms, + self.total_overhead_ms, + ) return None # do not suppress exceptions diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index c7c40439..0ad16303 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -13,7 +13,6 @@ RUN npm install COPY . . # Build the app -ENV NODE_OPTIONS="--max-old-space-size=4096" RUN npm run build # Runtime stage diff --git a/src/processor/Dockerfile b/src/processor/Dockerfile index 317c8e03..afe293a5 100644 --- a/src/processor/Dockerfile +++ b/src/processor/Dockerfile @@ -36,8 +36,7 @@ RUN curl -fsSLo /tmp/node.tar.gz "https://nodejs.org/dist/v${NODE_VERSION}/node- COPY pyproject.toml uv.lock ./ # Install dependencies using UV -# Re-lock to pick up any pyproject.toml changes (e.g. new deps), then install. -RUN uv lock --python 3.12 && uv sync --frozen --python 3.12 +RUN uv sync --frozen --python 3.12 # Copy the entire source code COPY src/ ./src/ diff --git a/src/processor/src/tests/test_llm_token_telemetry.py b/src/processor/src/tests/test_llm_token_telemetry.py deleted file mode 100644 index 6a24f5b1..00000000 --- a/src/processor/src/tests/test_llm_token_telemetry.py +++ /dev/null @@ -1,572 +0,0 @@ -"""Unit tests for app.utils.llm_token_telemetry. - -Covers: -- TokenUsage arithmetic and realtime sub-fields -- All extractors (dict / object / raw_representation / aggregated messages / - streaming chunks / realtime / Mock-input safety) -- detect_invoked_tools -- TokenUsageEmitter: enabled/disabled, sink-throws-doesn't-propagate, - static_dimensions merge, all typed emitters, emit_all distinct models -- TokenUsageScope: happy path, exception in body still emits, multi-add -""" -from __future__ import annotations - -import logging -from unittest.mock import Mock - -import pytest - -from app.utils.llm_token_telemetry import ( - EVENT_AGENT, - EVENT_MODEL, - EVENT_SPEECH, - EVENT_SUMMARY, - TokenUsage, - TokenUsageEmitter, - TokenUsageScope, - detect_invoked_tools, - extract_realtime_usage, - extract_usage, - extract_usage_from_dict, - extract_usage_from_stream_chunk, -) - - -# --------------------------------------------------------------------------- -# TokenUsage -# --------------------------------------------------------------------------- -class TestTokenUsage: - def test_has_any_false_when_zero(self): - assert TokenUsage().has_any is False - - def test_has_any_true_when_any_nonzero(self): - assert TokenUsage(input_tokens=1).has_any is True - assert TokenUsage(total_tokens=5).has_any is True - - def test_addition_basic(self): - a = TokenUsage(1, 2, 3) - b = TokenUsage(4, 5, 9) - assert a + b == TokenUsage(5, 7, 12) - - def test_addition_realtime_subfields(self): - a = TokenUsage(1, 2, 3, input_audio_tokens=10) - b = TokenUsage(4, 5, 9, input_audio_tokens=20, output_audio_tokens=7) - c = a + b - assert c.input_audio_tokens == 30 - assert c.output_audio_tokens == 7 # None + 7 -> 7 - - def test_addition_returns_notimplemented_for_other_types(self): - assert TokenUsage(1).__add__("nope") is NotImplemented - - def test_to_event_props_omits_none_subfields(self): - props = TokenUsage(1, 2, 3).to_event_props() - assert props == {"input_tokens": "1", "output_tokens": "2", "total_tokens": "3"} - - def test_to_event_props_includes_realtime_when_present(self): - props = TokenUsage(1, 2, 3, input_audio_tokens=4).to_event_props() - assert props["input_audio_tokens"] == "4" - - -# --------------------------------------------------------------------------- -# extract_usage_from_dict -# --------------------------------------------------------------------------- -class TestExtractFromDict: - @pytest.mark.parametrize("data,expected", [ - ({"prompt_tokens": 12, "completion_tokens": 8}, (12, 8, 20)), - ({"input_tokens": 5, "output_tokens": 7, "total_tokens": 12}, (5, 7, 12)), - ({"input_token_count": 3, "output_token_count": 4}, (3, 4, 7)), - ({"promptTokens": 1, "completionTokens": 2, "totalTokens": 3}, (1, 2, 3)), - ]) - def test_aliases(self, data, expected): - u = extract_usage_from_dict(data) - assert (u.input_tokens, u.output_tokens, u.total_tokens) == expected - - def test_none_returns_none(self): - assert extract_usage_from_dict(None) is None - - def test_empty_returns_none(self): - assert extract_usage_from_dict({}) is None - - def test_total_falls_back_to_sum(self): - u = extract_usage_from_dict({"input_tokens": 4, "output_tokens": 6}) - assert u.total_tokens == 10 - - def test_string_digits_coerced(self): - u = extract_usage_from_dict({"input_tokens": "10", "output_tokens": "20"}) - assert u.input_tokens == 10 - assert u.output_tokens == 20 - - -# --------------------------------------------------------------------------- -# extract_usage (object shapes) -# --------------------------------------------------------------------------- -class _Bag: - """Minimal attribute bag (acts like an SDK model object).""" - pass - - -class TestExtractUsage: - def test_usage_details_dict(self): - r = _Bag() - r.usage_details = {"input_token_count": 5, "output_token_count": 7} - u = extract_usage(r) - assert u.total_tokens == 12 - - def test_usage_details_object(self): - r = _Bag() - details = _Bag() - details.input_token_count = 5 - details.output_token_count = 7 - details.total_token_count = 12 - r.usage_details = details - u = extract_usage(r) - assert u.total_tokens == 12 - - def test_raw_representation_openai_shape(self): - r = _Bag() - raw = _Bag() - raw.usage = {"prompt_tokens": 3, "completion_tokens": 4, "total_tokens": 7} - r.raw_representation = raw - u = extract_usage(r) - assert (u.input_tokens, u.output_tokens, u.total_tokens) == (3, 4, 7) - - def test_aggregated_messages(self): - r = _Bag() - msg = _Bag() - c1 = _Bag() - c1.usage_details = {"input_tokens": 2, "output_tokens": 3} - c2 = _Bag() - c2.usage_details = {"input_tokens": 4, "output_tokens": 1} - msg.contents = [c1, c2] - r.messages = [msg] - u = extract_usage(r) - assert u.input_tokens == 6 - assert u.output_tokens == 4 - - def test_none_input_returns_none(self): - assert extract_usage(None) is None - - def test_no_usage_returns_none(self): - assert extract_usage(_Bag()) is None - - def test_mock_input_does_not_raise(self): - """Mock objects expose every attribute as another Mock -- previously - this caused TypeError on iteration of .messages.""" - m = Mock() - # Should silently return None, never raise. - assert extract_usage(m) is None - - -# --------------------------------------------------------------------------- -# extract_usage_from_stream_chunk -# --------------------------------------------------------------------------- -class TestStreamChunk: - def test_chunk_with_metadata_usage(self): - c = _Bag() - c.metadata = {"usage": {"input_tokens": 1, "output_tokens": 2}} - u = extract_usage_from_stream_chunk(c) - assert u.input_tokens == 1 - assert u.output_tokens == 2 - - def test_no_usage_returns_none(self): - assert extract_usage_from_stream_chunk(_Bag()) is None - - -# --------------------------------------------------------------------------- -# extract_realtime_usage -# --------------------------------------------------------------------------- -class TestRealtime: - def test_basic(self): - r = _Bag() - r.usage = { - "input_tokens": 3, "output_tokens": 4, "total_tokens": 7, - "input_token_details": {"audio_tokens": 2, "text_tokens": 1, "cached_tokens": 0}, - "output_token_details": {"audio_tokens": 4, "text_tokens": 0}, - } - u = extract_realtime_usage(r) - assert u.input_audio_tokens == 2 - assert u.output_audio_tokens == 4 - assert u.total_tokens == 7 - - def test_total_derived_when_missing(self): - r = _Bag() - r.usage = {"input_tokens": 3, "output_tokens": 4} - u = extract_realtime_usage(r) - assert u.total_tokens == 7 - - def test_no_usage_returns_none(self): - assert extract_realtime_usage(_Bag()) is None - - -# --------------------------------------------------------------------------- -# detect_invoked_tools -# --------------------------------------------------------------------------- -class TestDetectInvokedTools: - def test_finds_function_calls(self): - r = _Bag() - c1 = _Bag() - c1.type = "function_call" - c1.name = "product_agent" - c2 = _Bag() - c2.type = "text" - c2.name = "n/a" - c3 = _Bag() - c3.type = "function_call" - c3.name = "policy_agent" - msg = _Bag() - msg.contents = [c1, c2, c3] - r.messages = [msg] - assert detect_invoked_tools(r) == {"product_agent", "policy_agent"} - - def test_empty_when_no_messages(self): - assert detect_invoked_tools(_Bag()) == set() - - def test_mock_input_safe(self): - assert detect_invoked_tools(Mock()) == set() - - def test_skips_function_calls_without_name(self): - r = _Bag() - c = _Bag() - c.type = "function_call" - c.name = None - msg = _Bag() - msg.contents = [c] - r.messages = [msg] - assert detect_invoked_tools(r) == set() - - -# --------------------------------------------------------------------------- -# TokenUsageEmitter -# --------------------------------------------------------------------------- -class TestEmitter: - def _make(self, **kw): - captured: list[tuple[str, dict]] = [] - kw.setdefault("connection_string", "fake-conn") - kw.setdefault("event_sink", lambda n, p: captured.append((n, dict(p)))) - em = TokenUsageEmitter(**kw) - return em, captured - - def test_disabled_when_no_connection_string(self): - em = TokenUsageEmitter(connection_string="", event_sink=lambda *a: None) - assert em.enabled is False - - def test_disabled_when_no_sink(self): - em = TokenUsageEmitter(connection_string="x", event_sink=None) - # _default_event_sink may or may not be available; force-disable: - em._sink = None - assert em.enabled is False - - def test_static_dimensions_prestringified_and_merged(self): - em, captured = self._make(static_dimensions={"app": "x", "tenant": 42}) - em.emit("X", user_id="u1") - name, props = captured[0] - assert name == "X" - assert props["app"] == "x" - assert props["tenant"] == "42" # stringified - assert props["user_id"] == "u1" - - def test_call_dimension_overrides_static(self): - em, captured = self._make(static_dimensions={"app": "default"}) - em.emit("X", app="override") - assert captured[0][1]["app"] == "override" - - def test_none_dimension_dropped(self): - em, captured = self._make() - em.emit("X", user_id=None, session_id="s1") - assert "user_id" not in captured[0][1] - assert captured[0][1]["session_id"] == "s1" - - def test_sink_exception_does_not_propagate(self, caplog): - def boom(_n, _p): - raise RuntimeError("sink broken") - em = TokenUsageEmitter(connection_string="x", event_sink=boom) - with caplog.at_level(logging.WARNING): - em.emit("X") # must not raise - - def test_emit_agent_skips_zero_usage(self): - em, captured = self._make() - em.emit_agent(agent_name="a", model_deployment_name="m", usage=TokenUsage()) - assert captured == [] - - def test_emit_agent_populates_props(self): - em, captured = self._make() - em.emit_agent(agent_name="chat", model_deployment_name="gpt-4o", - usage=TokenUsage(10, 20, 30), user_id="u") - name, props = captured[0] - assert name == EVENT_AGENT - assert props["agent_name"] == "chat" - assert props["model_deployment_name"] == "gpt-4o" - assert props["total_tokens"] == "30" - assert props["user_id"] == "u" - - def test_emit_all_emits_summary_agent_and_per_distinct_model(self): - em, captured = self._make() - em.emit_all( - agent_name="orchestrator", - model_deployment_name="gpt-4o", - usage=TokenUsage(10, 20, 30), - additional_agents={"tool_a": "gpt-4o", "tool_b": "gpt-35"}, - user_id="u1", - ) - names = [n for n, _ in captured] - # exactly one summary + one agent + two model events (gpt-4o, gpt-35) - assert names.count(EVENT_SUMMARY) == 1 - assert names.count(EVENT_AGENT) == 1 - assert names.count(EVENT_MODEL) == 2 - # summary records agent + model counts - summary = next(p for n, p in captured if n == EVENT_SUMMARY) - assert summary["agent_count"] == "3" - assert summary["model_count"] == "2" - assert summary["total_input_tokens"] == "10" - - def test_emit_speech_includes_audio_subfields(self): - em, captured = self._make() - em.emit_speech( - model_deployment_name="gpt-4o-realtime", - source="voice_chat", - usage=TokenUsage(1, 2, 3, input_audio_tokens=5, output_audio_tokens=6), - ) - name, props = captured[0] - assert name == EVENT_SPEECH - assert props["source"] == "voice_chat" - assert props["input_audio_tokens"] == "5" - assert props["output_audio_tokens"] == "6" - - -# --------------------------------------------------------------------------- -# Pricing / cost computation -# --------------------------------------------------------------------------- -class TestPricing: - def _make(self, pricing): - captured: list[tuple[str, dict]] = [] - em = TokenUsageEmitter( - connection_string="x", - event_sink=lambda n, p: captured.append((n, dict(p))), - pricing=pricing, - ) - return em, captured - - def test_cost_attached_to_agent_event(self): - em, captured = self._make({"gpt-4o": (0.0025, 0.01)}) - em.emit_agent(agent_name="a", model_deployment_name="gpt-4o", - usage=TokenUsage(1000, 500, 1500)) - # 1000 * 0.0025/1k + 500 * 0.01/1k = 0.0025 + 0.005 = 0.0075 - assert captured[0][1]["estimated_cost_usd"] == "0.007500" - - def test_cost_case_insensitive_model_lookup(self): - em, captured = self._make({"GPT-4o": (0.001, 0.001)}) - em.emit_model(model_deployment_name="gpt-4o", - usage=TokenUsage(1000, 1000, 2000)) - assert "estimated_cost_usd" in captured[0][1] - - def test_no_cost_when_model_unknown(self): - em, captured = self._make({"gpt-4o": (0.001, 0.001)}) - em.emit_agent(agent_name="a", model_deployment_name="gpt-mystery", - usage=TokenUsage(10, 10, 20)) - assert "estimated_cost_usd" not in captured[0][1] - - def test_summary_picks_up_cost_via_emit_all(self): - em, captured = self._make({"gpt-4o": (0.0025, 0.01)}) - em.emit_all(agent_name="chat", model_deployment_name="gpt-4o", - usage=TokenUsage(1000, 500, 1500)) - summary = next(p for n, p in captured if n == EVENT_SUMMARY) - assert summary["estimated_cost_usd"] == "0.007500" - - def test_malformed_pricing_entry_ignored(self, caplog): - with caplog.at_level(logging.WARNING): - em = TokenUsageEmitter( - connection_string="x", - event_sink=lambda *a: None, - pricing={"bad-model": "not-a-tuple"}, # type: ignore[dict-item] - ) - # Emitter still constructs; bad entry skipped. - assert "bad-model" not in em._pricing - - -# --------------------------------------------------------------------------- -# user_id PII hashing -# --------------------------------------------------------------------------- -class TestUserIdHasher: - def _make(self, hasher): - captured: list[tuple[str, dict]] = [] - em = TokenUsageEmitter( - connection_string="x", - event_sink=lambda n, p: captured.append((n, dict(p))), - user_id_hasher=hasher, - ) - return em, captured - - def test_hasher_applied_to_call_kwargs(self): - em, captured = self._make(lambda v: f"H({v})") - em.emit("X", user_id="alice") - assert captured[0][1]["user_id"] == "H(alice)" - - def test_hasher_applied_to_static_dimensions_at_construction(self): - em = TokenUsageEmitter( - connection_string="x", - event_sink=lambda *a: None, - user_id_hasher=lambda v: f"H({v})", - static_dimensions={"user_id": "bob"}, - ) - assert em._static["user_id"] == "H(bob)" - - def test_hasher_exception_falls_back_to_raw(self, caplog): - def boom(_v): - raise RuntimeError("hasher broken") - em, captured = self._make(boom) - with caplog.at_level(logging.WARNING): - em.emit("X", user_id="alice") - # Falls back to original value -- never breaks telemetry. - assert captured[0][1]["user_id"] == "alice" - - def test_no_hasher_passes_through(self): - em, captured = self._make(None) - em.emit("X", user_id="alice") - assert captured[0][1]["user_id"] == "alice" - - def test_empty_user_id_not_hashed_or_emitted(self): - em, captured = self._make(lambda v: f"H({v})") - em.emit("X", user_id="") - # Empty user_id should be dropped, not hashed to "H()". - assert "user_id" not in captured[0][1] - - -# --------------------------------------------------------------------------- -# Sampling -# --------------------------------------------------------------------------- -class TestSampling: - def _make(self, rate): - captured: list[tuple[str, dict]] = [] - em = TokenUsageEmitter( - connection_string="x", - event_sink=lambda n, p: captured.append((n, dict(p))), - sample_rate=rate, - ) - return em, captured - - def test_rate_clamped_to_unit_interval(self): - assert TokenUsageEmitter(connection_string="x", sample_rate=-0.5, - event_sink=lambda *a: None).sample_rate == 0.0 - assert TokenUsageEmitter(connection_string="x", sample_rate=2.0, - event_sink=lambda *a: None).sample_rate == 1.0 - - def test_invalid_rate_defaults_to_one(self): - em = TokenUsageEmitter(connection_string="x", sample_rate="nope", # type: ignore[arg-type] - event_sink=lambda *a: None) - assert em.sample_rate == 1.0 - - def test_zero_rate_drops_agent_event(self): - em, captured = self._make(0.0) - em.emit_agent(agent_name="a", model_deployment_name="m", - usage=TokenUsage(1, 2, 3)) - assert captured == [] - - def test_zero_rate_still_emits_summary(self): - em, captured = self._make(0.0) - em.emit_summary(usage=TokenUsage(1, 2, 3)) - assert captured and captured[0][0] == EVENT_SUMMARY - - def test_summary_records_sample_rate(self): - em, captured = self._make(0.25) - em.emit_summary(usage=TokenUsage(1, 2, 3)) - assert captured[0][1]["sample_rate"] == "0.2500" - - def test_emit_all_with_zero_rate_only_emits_summary(self): - em, captured = self._make(0.0) - em.emit_all(agent_name="chat", model_deployment_name="gpt-4o", - usage=TokenUsage(10, 20, 30)) - assert [n for n, _ in captured] == [EVENT_SUMMARY] - - def test_full_rate_emits_everything(self): - em, captured = self._make(1.0) - em.emit_all(agent_name="chat", model_deployment_name="gpt-4o", - usage=TokenUsage(10, 20, 30), - additional_agents={"a2": "gpt-35"}) - names = [n for n, _ in captured] - assert EVENT_SUMMARY in names - assert EVENT_AGENT in names - assert names.count(EVENT_MODEL) == 2 - - -# --------------------------------------------------------------------------- -# TokenUsageScope (continued) -# --------------------------------------------------------------------------- -class TestScope: - def _emitter(self): - captured: list[tuple[str, dict]] = [] - em = TokenUsageEmitter( - connection_string="x", - event_sink=lambda n, p: captured.append((n, dict(p))), - ) - return em, captured - - def test_happy_path_emits_on_exit(self): - em, captured = self._emitter() - r = _Bag() - r.usage_details = {"input_tokens": 1, "output_tokens": 2, "total_tokens": 3} - with TokenUsageScope(em, agent_name="a", model_deployment_name="m") as s: - s.add(r) - assert any(n == EVENT_SUMMARY for n, _ in captured) - assert any(n == EVENT_AGENT for n, _ in captured) - - def test_multi_add_accumulates(self): - em, captured = self._emitter() - r1 = _Bag() - r1.usage_details = {"input_tokens": 1, "output_tokens": 2} - r2 = _Bag() - r2.usage_details = {"input_tokens": 4, "output_tokens": 5} - with TokenUsageScope(em, agent_name="a", model_deployment_name="m") as s: - s.add(r1) - s.add(r2) - agent = next(p for n, p in captured if n == EVENT_AGENT) - assert agent["input_tokens"] == "5" - assert agent["output_tokens"] == "7" - assert agent["total_tokens"] == "12" - - def test_exception_in_body_still_emits(self): - em, captured = self._emitter() - r = _Bag() - r.usage_details = {"input_tokens": 1, "output_tokens": 2, "total_tokens": 3} - with pytest.raises(ValueError): - with TokenUsageScope(em, agent_name="a", model_deployment_name="m") as s: - s.add(r) - raise ValueError("boom") - # Emission still happened - assert any(n == EVENT_AGENT for n, _ in captured) - - def test_add_with_mock_does_not_raise(self): - em, _ = self._emitter() - with TokenUsageScope(em, agent_name="a", model_deployment_name="m") as s: - assert s.add(Mock()) is None - - def test_zero_usage_does_not_emit(self): - em, captured = self._emitter() - with TokenUsageScope(em, agent_name="a", model_deployment_name="m"): - pass - assert captured == [] - - def test_dimensions_flow_to_events(self): - em, captured = self._emitter() - r = _Bag() - r.usage_details = {"input_tokens": 1, "output_tokens": 2, "total_tokens": 3} - with TokenUsageScope(em, agent_name="a", model_deployment_name="m", - user_id="u1", session_id="s1") as s: - s.add(r) - for _, p in captured: - assert p["user_id"] == "u1" - assert p["session_id"] == "s1" - - def test_additional_agents_after_scope_open(self): - em, captured = self._emitter() - r = _Bag() - r.usage_details = {"input_tokens": 1, "output_tokens": 2, "total_tokens": 3} - with TokenUsageScope(em, agent_name="orchestrator", - model_deployment_name="gpt-4o") as s: - s.add(r) - # Mutate additional_agents after the call -- mirrors the - # detect_invoked_tools usage pattern. - s.additional_agents["tool_a"] = "gpt-35" - model_events = [p for n, p in captured if n == EVENT_MODEL] - models = {p["model_deployment_name"] for p in model_events} - assert models == {"gpt-4o", "gpt-35"} - diff --git a/src/processor/src/utils/llm_token_telemetry.py b/src/processor/src/utils/llm_token_telemetry.py index b3035fc8..91f670c5 100644 --- a/src/processor/src/utils/llm_token_telemetry.py +++ b/src/processor/src/utils/llm_token_telemetry.py @@ -53,6 +53,7 @@ import logging import os import random +import time from contextlib import AbstractContextManager from dataclasses import dataclass, field from typing import Any, Callable, Iterable, Mapping, Optional @@ -472,6 +473,16 @@ def __init__( k: ("" if v is None else str(v)) for k, v in raw_static.items() } + # Performance counters. ``perf_*`` accumulate wall-clock nanoseconds + # spent inside ``emit()`` so callers can verify telemetry overhead is + # negligible. ``perf_slow_emit_threshold_ms`` is the soft threshold + # above which a WARNING is logged for an individual emit (default + # 50 ms -- emits should normally take well under 1 ms). + self._perf_total_ns: int = 0 + self._perf_emit_count: int = 0 + self._perf_max_ns: int = 0 + self.perf_slow_emit_threshold_ms: float = 50.0 + # -- public surface --------------------------------------------------- @property def enabled(self) -> bool: @@ -539,28 +550,74 @@ def emit(self, event_name: str, **dimensions: Any) -> None: Non-string values are stringified. ``None`` values are dropped. Any ``user_id`` value is passed through the configured hasher. - Never raises. + Never raises. Wall-clock duration is recorded for performance audit + (see :meth:`perf_stats`). """ - props = dict(self._static) # cheap shallow copy of pre-stringified dims - for k, v in dimensions.items(): - if v is None: - continue - if k == "user_id": - v = self._apply_user_id_hash(v) - if v is None or v == "": + start_ns = time.perf_counter_ns() + try: + props = dict(self._static) # cheap shallow copy of pre-stringified dims + for k, v in dimensions.items(): + if v is None: continue - props[k] = v if isinstance(v, str) else str(v) + if k == "user_id": + v = self._apply_user_id_hash(v) + if v is None or v == "": + continue + props[k] = v if isinstance(v, str) else str(v) + + if not self.enabled: + self._log.debug( + "App Insights not configured -- skipping event %s (%s)", + event_name, props, + ) + return + try: + self._sink(event_name, props) # type: ignore[misc] + except Exception as exc: # never break the caller + self._log.warning("track_event(%s) failed: %s", event_name, exc) + finally: + elapsed_ns = time.perf_counter_ns() - start_ns + self._perf_total_ns += elapsed_ns + self._perf_emit_count += 1 + if elapsed_ns > self._perf_max_ns: + self._perf_max_ns = elapsed_ns + elapsed_ms = elapsed_ns / 1_000_000.0 + if elapsed_ms > self.perf_slow_emit_threshold_ms: + self._log.warning( + "Token telemetry emit slow: event=%s duration_ms=%.3f", + event_name, elapsed_ms, + ) + else: + self._log.debug( + "Token telemetry emit: event=%s duration_ms=%.3f", + event_name, elapsed_ms, + ) + + # -- performance audit ------------------------------------------------ + def perf_stats(self) -> dict[str, float]: + """Return cumulative telemetry-overhead stats since process start + (or since :meth:`reset_perf_stats`). + + Keys: + ``emit_count`` -- number of events emitted + ``total_ms`` -- total wall-clock time spent inside ``emit`` + ``avg_ms`` -- mean per-event duration + ``max_ms`` -- slowest single emit observed + """ + count = self._perf_emit_count + total_ms = self._perf_total_ns / 1_000_000.0 + return { + "emit_count": float(count), + "total_ms": total_ms, + "avg_ms": (total_ms / count) if count else 0.0, + "max_ms": self._perf_max_ns / 1_000_000.0, + } - if not self.enabled: - self._log.debug( - "App Insights not configured -- skipping event %s (%s)", - event_name, props, - ) - return - try: - self._sink(event_name, props) # type: ignore[misc] - except Exception as exc: # never break the caller - self._log.warning("track_event(%s) failed: %s", event_name, exc) + def reset_perf_stats(self) -> None: + """Zero the perf counters (useful for tests and load-tests).""" + self._perf_total_ns = 0 + self._perf_emit_count = 0 + self._perf_max_ns = 0 # -- typed convenience emitters -------------------------------------- def emit_agent( @@ -713,14 +770,12 @@ def emit_all( agents.update({k: v for k, v in additional_agents.items() if k}) models = {m for m in agents.values() if m} - self.emit_summary( - usage=usage, - agent_count=len(agents), - model_count=len(models) or 1, - primary_model=model_deployment_name, - additional_agents=additional_agents, - **dimensions, - ) + # Wall-clock timing of the whole emit_all path so callers (or tests) + # can verify the telemetry path stays cheap relative to the LLM call + # it instruments. + batch_start_ns = time.perf_counter_ns() + + # Defer summary until last so we can stamp the batch overhead on it. self.emit_agent( agent_name=agent_name, model_deployment_name=model_deployment_name, @@ -748,6 +803,17 @@ def emit_all( model_deployment_name=model_deployment_name, ) + batch_overhead_ms = (time.perf_counter_ns() - batch_start_ns) / 1_000_000.0 + self.emit_summary( + usage=usage, + agent_count=len(agents), + model_count=len(models) or 1, + primary_model=model_deployment_name, + additional_agents=additional_agents, + telemetry_overhead_ms=f"{batch_overhead_ms:.3f}", + **dimensions, + ) + self._log.info( "[TOKEN USAGE] agent=%s model=%s input=%d output=%d total=%d %s", agent_name, @@ -804,6 +870,12 @@ def __init__( self.emit_team_event = emit_team_event self.dimensions = dict(dimensions) self.usage = TokenUsage() + # Wall-clock nanoseconds spent inside extraction (``add*``) and the + # final ``__exit__`` emit, respectively. Surfaced for callers that + # want to verify the helper doesn't add measurable latency. Available + # as ``scope.extract_ms`` / ``scope.emit_ms`` after the scope closes. + self._extract_ns: int = 0 + self._emit_ns: int = 0 # -- accumulation ----------------------------------------------------- def add(self, source: Any) -> Optional[TokenUsage]: @@ -812,11 +884,14 @@ def add(self, source: Any) -> Optional[TokenUsage]: Never raises -- extraction failures return ``None`` and are logged at DEBUG. """ + start_ns = time.perf_counter_ns() try: found = extract_usage(source) or extract_usage_from_stream_chunk(source) except Exception as exc: # belt + braces; extractors are already safe logger.debug("TokenUsageScope.add failed: %s", exc, exc_info=True) return None + finally: + self._extract_ns += time.perf_counter_ns() - start_ns if found: self.usage = self.usage + found return found @@ -828,9 +903,26 @@ def add_chunks(self, chunks: Iterable[Any]) -> None: for c in chunks: self.add(c) + # -- timing properties ----------------------------------------------- + @property + def extract_ms(self) -> float: + """Total ms spent inside :meth:`add` / :meth:`add_chunks`.""" + return self._extract_ns / 1_000_000.0 + + @property + def emit_ms(self) -> float: + """Total ms spent in the on-exit emit batch.""" + return self._emit_ns / 1_000_000.0 + + @property + def total_overhead_ms(self) -> float: + """Total telemetry overhead added by this scope (extract + emit).""" + return self.extract_ms + self.emit_ms + # -- context manager -------------------------------------------------- def __exit__(self, exc_type, exc, tb) -> None: # Always emit (best-effort) regardless of exception status. + emit_start_ns = time.perf_counter_ns() try: self.emitter.emit_all( agent_name=self.agent_name, @@ -843,6 +935,16 @@ def __exit__(self, exc_type, exc, tb) -> None: ) except Exception as emit_exc: # pragma: no cover - belt + braces logger.warning("TokenUsageScope emit failed: %s", emit_exc) + finally: + self._emit_ns += time.perf_counter_ns() - emit_start_ns + logger.debug( + "TokenUsageScope overhead: agent=%s extract_ms=%.3f " + "emit_ms=%.3f total_ms=%.3f", + self.agent_name, + self.extract_ms, + self.emit_ms, + self.total_overhead_ms, + ) return None # do not suppress exceptions From 4393540fa73d310b9b071407adeeed404a902be8 Mon Sep 17 00:00:00 2001 From: Priyanka-Microsoft Date: Mon, 1 Jun 2026 14:10:27 +0530 Subject: [PATCH 11/50] fix: update uv.lock to include azure-monitor-events-extension The lockfile was missing the azure-monitor-events-extension and azure-monitor-opentelemetry packages, causing uv sync --frozen in Docker to skip them. This made the emitter silently disabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/processor/uv.lock | 474 +++++++++++++++++++++++++++++++++++------- 1 file changed, 398 insertions(+), 76 deletions(-) diff --git a/src/processor/uv.lock b/src/processor/uv.lock index dec50656..f95de739 100644 --- a/src/processor/uv.lock +++ b/src/processor/uv.lock @@ -424,9 +424,9 @@ name = "aiologic" version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio" }, + { name = "sniffio", marker = "python_full_version < '3.13'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, - { name = "wrapt" }, + { name = "wrapt", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/13/50b91a3ea6b030d280d2654be97c48b6ed81753a50286ee43c646ba36d3c/aiologic-0.16.0.tar.gz", hash = "sha256:c267ccbd3ff417ec93e78d28d4d577ccca115d5797cdbd16785a551d9658858f", size = 225952, upload-time = "2025-11-27T23:48:41.195Z" } wheels = [ @@ -505,6 +505,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/29/57b06fdb3abdf52c621d3ca3caea735e2db4c8d48288ebd26af448e8e247/art-6.5-py3-none-any.whl", hash = "sha256:70706408144c45c666caab690627d5c74aea7b6c7ce8cc968408ddeef8d84afd", size = 610382, upload-time = "2025-04-12T17:02:21.97Z" }, ] +[[package]] +name = "asgiref" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, +] + [[package]] name = "asyncio" version = "4.0.0" @@ -627,6 +636,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825, upload-time = "2026-01-12T17:03:07.291Z" }, ] +[[package]] +name = "azure-core-tracing-opentelemetry" +version = "1.0.0b13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/ab/a937e4af8afec9d437d55252f2a3a4419fc3fc7d5e5d54022622bd11b2b6/azure_core_tracing_opentelemetry-1.0.0b13.tar.gz", hash = "sha256:6cb2f8dfd5dee6c11843db0205fc92e2434e1a272c169c953afe92483aafc7eb", size = 25832, upload-time = "2026-05-01T00:59:57.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/01/8898c2506cae6a57c1b76d930d2af94764a65354bc863feb2684235851ce/azure_core_tracing_opentelemetry-1.0.0b13-py3-none-any.whl", hash = "sha256:4dacd3a9f117f11f98e89305e161c951b8df85b984f3b56130614de9cd9887f9", size = 12112, upload-time = "2026-05-01T00:59:59.149Z" }, +] + [[package]] name = "azure-cosmos" version = "4.15.0" @@ -706,6 +728,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/28/af9ef022f21e3b51b3718d4348f771b490678c1116563895547c0a771362/azure_identity-1.26.0b1-py3-none-any.whl", hash = "sha256:dc608b59ae628a38611208ee761adeb1a2b9390258b58d6edcda2d24c50a4348", size = 197227, upload-time = "2025-11-07T03:04:16.923Z" }, ] +[[package]] +name = "azure-monitor-events-extension" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/51/976c8cd4a76d41bcd4d3f6400aeed8fdd70d516d271badf9c4a5893a558d/azure-monitor-events-extension-0.1.0.tar.gz", hash = "sha256:094773685171a50aa5cc548279c9141c8a26682f6acef397815c528b53b838b5", size = 4165, upload-time = "2023-09-19T20:01:17.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/44/cbb68c55505a604de61caa44375be7371368e71aa8386b1576be5b789e11/azure_monitor_events_extension-0.1.0-py2.py3-none-any.whl", hash = "sha256:5d92abb5e6a32ab23b12c726def9f9607c6fa1d84900d493b906ff9ec489af4a", size = 4514, upload-time = "2023-09-19T20:01:16.162Z" }, +] + +[[package]] +name = "azure-monitor-opentelemetry" +version = "1.8.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "azure-core-tracing-opentelemetry" }, + { name = "azure-monitor-opentelemetry-exporter" }, + { name = "opentelemetry-instrumentation-django" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-flask" }, + { name = "opentelemetry-instrumentation-logging" }, + { name = "opentelemetry-instrumentation-psycopg2" }, + { name = "opentelemetry-instrumentation-requests" }, + { name = "opentelemetry-instrumentation-urllib" }, + { name = "opentelemetry-instrumentation-urllib3" }, + { name = "opentelemetry-resource-detector-azure" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/42/ea67bebb400a7561b1ad1dd59d06b67e880daf8081ec0d41d3b0ce8fcc26/azure_monitor_opentelemetry-1.8.7.tar.gz", hash = "sha256:d0a430c69451f8fa09362769d2d65471713989fb78e4ad0f50832b597921efbb", size = 76970, upload-time = "2026-03-19T21:43:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/22/245a4f75a834430759a6fab9c5ab10e18719786ae684cf234c7bb6a693d1/azure_monitor_opentelemetry-1.8.7-py3-none-any.whl", hash = "sha256:0d3a228a183d76cf22698a3eed6e836d1cf57608b8ee879c634609b26f384eb2", size = 41268, upload-time = "2026-03-19T21:43:58.188Z" }, +] + +[[package]] +name = "azure-monitor-opentelemetry-exporter" +version = "1.0.0b52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "azure-identity" }, + { name = "msrest" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "psutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/7e/bfc03436b88c48f5adc21a3ebbf4392b6b7fbbfe33ef3b1e88d07ba9f380/azure_monitor_opentelemetry_exporter-1.0.0b52.tar.gz", hash = "sha256:7eac679fca32dee9e426df65f2a538161db4514fc322fc66107f7826567d86e1", size = 326179, upload-time = "2026-05-11T22:47:02.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/e8/d13e6a74c98ecc3011bce9ab09fc2e75aec48ab46288f72be57c2fa21460/azure_monitor_opentelemetry_exporter-1.0.0b52-py2.py3-none-any.whl", hash = "sha256:a38c503e5e2cc0ec8a4bf336b23cce23488719f5361a45cdd01a514080f0e7fc", size = 244751, upload-time = "2026-05-11T22:47:04.304Z" }, +] + [[package]] name = "azure-search-documents" version = "11.7.0b2" @@ -1015,7 +1091,7 @@ name = "clr-loader" version = "0.2.10" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi" }, + { name = "cffi", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" } wheels = [ @@ -1097,7 +1173,7 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic" }, + { name = "aiologic", marker = "python_full_version < '3.13'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } @@ -2124,6 +2200,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, ] +[[package]] +name = "msrest" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "certifi" }, + { name = "isodate" }, + { name = "requests" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332, upload-time = "2022-06-13T22:41:25.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384, upload-time = "2022-06-13T22:41:22.42Z" }, +] + [[package]] name = "multidict" version = "6.7.1" @@ -2336,6 +2428,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, ] +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + [[package]] name = "ollama" version = "0.6.2" @@ -2416,42 +2517,237 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.41.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/37/6bf8e66bfcee5d3c6515b79cb2ee9ad05fe573c20f7ceb288d0e7eeec28c/opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7", size = 32606, upload-time = "2026-03-04T14:20:16.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/3e/f6f10f178b6316de67f0dfdbbb699a24fbe8917cf1743c1595fb9dcdd461/opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63", size = 33448, upload-time = "2026-03-04T14:19:02.447Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/3e/143cf5c034e58037307e6a24f06e0dd64b2c49ae60a965fc580027581931/opentelemetry_instrumentation_asgi-0.61b0.tar.gz", hash = "sha256:9d08e127244361dc33976d39dd4ca8f128b5aa5a7ae425208400a80a095019b5", size = 26691, upload-time = "2026-03-04T14:20:21.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/78/154470cf9d741a7487fbb5067357b87386475bbb77948a6707cae982e158/opentelemetry_instrumentation_asgi-0.61b0-py3-none-any.whl", hash = "sha256:e4b3ce6b66074e525e717efff20745434e5efd5d9df6557710856fba356da7a4", size = 16980, upload-time = "2026-03-04T14:19:10.894Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-dbapi" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/ed/ba91c9e4a3ec65781e9c59982109f0a36de9fa574f622596b33d1985dab5/opentelemetry_instrumentation_dbapi-0.61b0.tar.gz", hash = "sha256:02fa800682c1de87dcad0e59f2092b3b6fb8b8ea0636518f989e1166b418dcb9", size = 16761, upload-time = "2026-03-04T14:20:29.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/a5/d26c68f3fd33eb7410985cef7700bb426e2c4a26de9207902cbbffb19a3f/opentelemetry_instrumentation_dbapi-0.61b0-py3-none-any.whl", hash = "sha256:8f762c39c8edd20c6aef3282550a2cfbfec76c3f431bf5c36327dcf9ece2e5a0", size = 14134, upload-time = "2026-03-04T14:19:24.718Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-django" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-wsgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/ef/6bc1a6560630f26b1c010af86b28f42bfbe6a601bd1647d1436e0d3436aa/opentelemetry_instrumentation_django-0.61b0.tar.gz", hash = "sha256:9885154dc128578de0e6b5ce49e965c786f8ab071175bec005dcd454510be951", size = 25996, upload-time = "2026-03-04T14:20:30.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/3b/74dad6d98fdee1d137f1c2748548d4159578508f21e3aef581c110e64041/opentelemetry_instrumentation_django-0.61b0-py3-none-any.whl", hash = "sha256:26c1b0b325a9783d4a2f4df660ba05cf929c3eda2ae9b07916b649bb44e1c5b6", size = 20773, upload-time = "2026-03-04T14:19:25.675Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/35/aa727bb6e6ef930dcdc96a617b83748fece57b43c47d83ba8d83fbeca657/opentelemetry_instrumentation_fastapi-0.61b0.tar.gz", hash = "sha256:3a24f35b07c557ae1bbc483bf8412221f25d79a405f8b047de8b670722e2fa9f", size = 24800, upload-time = "2026-03-04T14:20:32.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/05/acfeb2cccd434242a0a7d0ea29afaf077e04b42b35b485d89aee4e0d9340/opentelemetry_instrumentation_fastapi-0.61b0-py3-none-any.whl", hash = "sha256:a1a844d846540d687d377516b2ff698b51d87c781b59f47c214359c4a241047c", size = 13485, upload-time = "2026-03-04T14:19:30.351Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-flask" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-wsgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/33/d6852d8f2c3eef86f2f8c858d6f5315983c7063e07e595519e96d4c31c06/opentelemetry_instrumentation_flask-0.61b0.tar.gz", hash = "sha256:e9faf58dfd9860a1868442d180142645abdafc1a652dd73d469a5efd106a7d49", size = 24071, upload-time = "2026-03-04T14:20:33.437Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/41/619f3530324a58491f2d20f216a10dd7393629b29db4610dda642a27f4ed/opentelemetry_instrumentation_flask-0.61b0-py3-none-any.whl", hash = "sha256:e8ce474d7ce543bfbbb3e93f8a6f8263348af9d7b45502f387420cf3afa71253", size = 15996, upload-time = "2026-03-04T14:19:31.304Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/69473f925acfe2d4edf5c23bcced36906ac3627aa7c5722a8e3f60825f3b/opentelemetry_instrumentation_logging-0.61b0.tar.gz", hash = "sha256:feaa30b700acd2a37cc81db5f562ab0c3a5b6cc2453595e98b72c01dcf649584", size = 17906, upload-time = "2026-03-04T14:20:37.398Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0e/2137db5239cc5e564495549a4d11488a7af9b48fc76520a0eea20e69ddae/opentelemetry_instrumentation_logging-0.61b0-py3-none-any.whl", hash = "sha256:6d87e5ded6a0128d775d41511f8380910a1b610671081d16efb05ac3711c0074", size = 17076, upload-time = "2026-03-04T14:19:36.765Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-psycopg2" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-dbapi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/28/f28d52b1088e7a09761566f8700507b54d3d83a6f9c93c0ce02f53619e83/opentelemetry_instrumentation_psycopg2-0.61b0.tar.gz", hash = "sha256:863ccf9687b71e73dd489c7bb117278768bdf26aa0dafe7dc974a2425e05b5d7", size = 11676, upload-time = "2026-03-04T14:20:41.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f1/4341d0584c288765c73e28c30ba58e7aedb50c01108f17f947b872657f79/opentelemetry_instrumentation_psycopg2-0.61b0-py3-none-any.whl", hash = "sha256:36b96983beda05c927179bb66b6c72f07a8d9a591f76ce9da88b1dd1587cb083", size = 11491, upload-time = "2026-03-04T14:19:42.018Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/c7/7a47cb85c7aa93a9c820552e414889185bcf91245271d12e5d443e5f834d/opentelemetry_instrumentation_requests-0.61b0.tar.gz", hash = "sha256:15f879ce8fb206bd7e6fdc61663ea63481040a845218c0cf42902ce70bd7e9d9", size = 18379, upload-time = "2026-03-04T14:20:46.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/a1/a7a133b273d1f53950f16a370fc94367eff472c9c2576e8e9e28c62dcc9f/opentelemetry_instrumentation_requests-0.61b0-py3-none-any.whl", hash = "sha256:cce19b379949fe637eb73ba39b02c57d2d0805447ca6d86534aa33fcb141f683", size = 14207, upload-time = "2026-03-04T14:19:51.765Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-urllib" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/37/77cd326b083390e74280c08bbd585153809619dad068e2d1b253fec1164d/opentelemetry_instrumentation_urllib-0.61b0.tar.gz", hash = "sha256:6a15ff862fc1603e0ea5ea75558f76f36436b02e0ae48daecedcb5e574cce160", size = 16894, upload-time = "2026-03-04T14:20:52.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/fc/a88fbfd8b9eb16ba1c21f0514c12696441be7fc42c7e319f3ee793bf9e96/opentelemetry_instrumentation_urllib-0.61b0-py3-none-any.whl", hash = "sha256:d7e409876580fb41102e3522ce81a756e53a74073c036a267a1c280cc0fa09b0", size = 13970, upload-time = "2026-03-04T14:20:01.24Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-urllib3" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/80/7ad8da30f479c6117768e72d6f2f3f0bd3495338707d6f61de042149578a/opentelemetry_instrumentation_urllib3-0.61b0.tar.gz", hash = "sha256:f00037bc8ff813153c4b79306f55a14618c40469a69c6c03a3add29dc7e8b928", size = 19325, upload-time = "2026-03-04T14:20:53.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/0c/01359e55b9f2fb2b1d4d9e85e77773a96697207895118533f3be718a3326/opentelemetry_instrumentation_urllib3-0.61b0-py3-none-any.whl", hash = "sha256:9644f8c07870266e52f129e6226859ff3a35192555abe46fa0ef9bbbf5b6b46d", size = 14339, upload-time = "2026-03-04T14:20:02.681Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-wsgi" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/e5/189f2845362cfe78e356ba127eab21456309def411c6874aa4800c3de816/opentelemetry_instrumentation_wsgi-0.61b0.tar.gz", hash = "sha256:380f2ae61714e5303275a80b2e14c58571573cd1fddf496d8c39fb9551c5e532", size = 19898, upload-time = "2026-03-04T14:20:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/75/d6b42ba26f3c921be6d01b16561b7bb863f843bad7ac3a5011f62617bcab/opentelemetry_instrumentation_wsgi-0.61b0-py3-none-any.whl", hash = "sha256:bd33b0824166f24134a3400648805e8d2e6a7951f070241294e8b8866611d7fa", size = 14628, upload-time = "2026-03-04T14:20:03.934Z" }, +] + +[[package]] +name = "opentelemetry-resource-detector-azure" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e4/0d359d48d03d447225b30c3dd889d5d454e3b413763ff721f9b0e4ac2e59/opentelemetry_resource_detector_azure-0.1.5.tar.gz", hash = "sha256:e0ba658a87c69eebc806e75398cd0e9f68a8898ea62de99bc1b7083136403710", size = 11503, upload-time = "2024-05-16T21:54:58.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/ae/c26d8da88ba2e438e9653a408b0c2ad6f17267801250a8f3cc6405a93a72/opentelemetry_resource_detector_azure-0.1.5-py3-none-any.whl", hash = "sha256:4dcc5d54ab5c3b11226af39509bc98979a8b9e0f8a24c1b888783755d3bf00eb", size = 14252, upload-time = "2024-05-16T21:54:57.208Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.41.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", size = 230180, upload-time = "2026-04-24T13:15:50.991Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", size = 180213, upload-time = "2026-04-24T13:15:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.62b1" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", size = 145750, upload-time = "2026-04-24T13:15:52.236Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, ] [[package]] @@ -2467,6 +2763,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/22/41fb05f1dc5fda2c468e05a41814c20859016c85117b66c8a257cae814f6/opentelemetry_semantic_conventions_ai-0.5.1-py3-none-any.whl", hash = "sha256:25aeb22bd261543b4898a73824026d96770e5351209c7d07a0b1314762b1f6e4", size = 11250, upload-time = "2026-03-26T14:20:37.108Z" }, ] +[[package]] +name = "opentelemetry-util-http" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/3c/f0196223efc5c4ca19f8fad3d5462b171ac6333013335ce540c01af419e9/opentelemetry_util_http-0.61b0.tar.gz", hash = "sha256:1039cb891334ad2731affdf034d8fb8b48c239af9b6dd295e5fabd07f1c95572", size = 11361, upload-time = "2026-03-04T14:20:57.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/e5/c08aaaf2f64288d2b6ef65741d2de5454e64af3e050f34285fb1907492fe/opentelemetry_util_http-0.61b0-py3-none-any.whl", hash = "sha256:8e715e848233e9527ea47e275659ea60a57a75edf5206a3b937e236a6da5fc33", size = 9281, upload-time = "2026-03-04T14:20:08.364Z" }, +] + [[package]] name = "orderedmultidict" version = "1.0.2" @@ -2574,8 +2879,8 @@ name = "powerfx" version = "0.0.34" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi" }, - { name = "pythonnet" }, + { name = "cffi", marker = "python_full_version < '3.14'" }, + { name = "pythonnet", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" } wheels = [ @@ -2613,6 +2918,8 @@ dependencies = [ { name = "azure-core" }, { name = "azure-cosmos" }, { name = "azure-identity" }, + { name = "azure-monitor-events-extension" }, + { name = "azure-monitor-opentelemetry" }, { name = "azure-storage-blob" }, { name = "azure-storage-file-datalake" }, { name = "azure-storage-queue" }, @@ -2646,6 +2953,8 @@ requires-dist = [ { name = "azure-core", specifier = "==1.38.0" }, { name = "azure-cosmos", specifier = "==4.15.0" }, { name = "azure-identity", specifier = "==1.26.0b1" }, + { name = "azure-monitor-events-extension", specifier = "==0.1.0" }, + { name = "azure-monitor-opentelemetry", specifier = "==1.8.7" }, { name = "azure-storage-blob", specifier = "==12.28.0" }, { name = "azure-storage-file-datalake", specifier = "==12.23.0" }, { name = "azure-storage-queue", specifier = "==12.15.0" }, @@ -3184,7 +3493,7 @@ name = "pythonnet" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "clr-loader" }, + { name = "clr-loader", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" } wheels = [ @@ -3389,6 +3698,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/53/ddb8b8fa96367976cf52bb0610ffd529bd7d2795b2e4c1724724d071718c/requests-2.34.0.dev1-py3-none-any.whl", hash = "sha256:c8749aeb3c4b204f80fd288f7507378c9afe66a3f189fb43fd77ea33e74d7564", size = 73077, upload-time = "2026-05-03T20:21:40.509Z" }, ] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + [[package]] name = "requests-toolbelt" version = "1.0.0" @@ -3985,66 +4307,66 @@ wheels = [ [[package]] name = "wrapt" -version = "2.2.0rc11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/d0/9c3b43631321c0fe61b9e2873b0542165a8f90393f49006f115d1e06eefc/wrapt-2.2.0rc11.tar.gz", hash = "sha256:fee2cf69591f32f16e5242ae4909bc9f43c66688c1f73f837c9c81313771ceba", size = 125088, upload-time = "2026-04-24T10:15:19.951Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/15/7b058ae7e7fe5bc042b3b0904a06a4038143113aca92684eed3e02f6a663/wrapt-2.2.0rc11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b9f264afa18c5fc12983698b4dcf66bdd521ff268f40d34db575b651c891d1e", size = 80950, upload-time = "2026-04-24T10:16:57.967Z" }, - { url = "https://files.pythonhosted.org/packages/b3/5e/605d3425b7533ee881ad4a3130699a7c48aba6e7134975438530ae7610a3/wrapt-2.2.0rc11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b5eddef9db8eb34e277b2dcf9ab4bd7898fcd8246380516cc34180496655e335", size = 81604, upload-time = "2026-04-24T10:17:04.913Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e2/91be7dcc9519fc35ce46b3b7955219ff99e219cd62eb43de89fa4d6653b4/wrapt-2.2.0rc11-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:469c67a6326a6a269f2cf391035bec7b4ab1aeac6acf56645e3b6c721a3153cc", size = 168642, upload-time = "2026-04-24T10:17:20.819Z" }, - { url = "https://files.pythonhosted.org/packages/f7/11/65135058543b659be3ae772c7510f56792968a752dfc617ac2577b0a5a5d/wrapt-2.2.0rc11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b4cd66a906667355bb72fbb7e2a7a1fe688671d6d68dad7efdbfa22ae165366", size = 170942, upload-time = "2026-04-24T10:16:25.138Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f8/83a59d35982ef5f26a2f0301d2f642b2063be8d20ec276b718a3f951b52c/wrapt-2.2.0rc11-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5df0ac11591b554fb028c93586cfb6991e3cefd5d8d5d0b0f6881dd1b3c1814c", size = 159962, upload-time = "2026-04-24T10:15:58.563Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1a/8f905774a951cc976d4a772c15329a3f542105265c24d6ce1e718d65dbaf/wrapt-2.2.0rc11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d74d14da54197a9b35eb6992212f7cdd6106e3bc75e69e2a9dc031e6ec806d9f", size = 168785, upload-time = "2026-04-24T10:17:35.07Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a3/61f54ea74c6d797271aa8e312bb5e0c98ef0cf39110aac863f579f2b5a83/wrapt-2.2.0rc11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b3c93ee7a1721234c00020662ecddab4fdd248fa8a2a12c9f0268c4bad085855", size = 158119, upload-time = "2026-04-24T10:16:54.622Z" }, - { url = "https://files.pythonhosted.org/packages/44/fc/4c36aaf560f273d53467a38fd91fda8ecc5aa5b4c96e495a9b33022d6daa/wrapt-2.2.0rc11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ee95ef8d643ddc7fbb76d8a1bffde5bb8c0f8a707d821fa9e95fde76e870d5d", size = 167068, upload-time = "2026-04-24T10:16:40.351Z" }, - { url = "https://files.pythonhosted.org/packages/5a/6c/5aeae8600e23ddd5e31f096351912caf5548bbff3799f846799d6b86f9d3/wrapt-2.2.0rc11-cp312-cp312-win32.whl", hash = "sha256:36a2f254c1e183d4404d8b816d453f639c1422d9a374cffb6e9e90e5ccb2a40c", size = 77804, upload-time = "2026-04-24T10:16:21.994Z" }, - { url = "https://files.pythonhosted.org/packages/99/70/c39167f608e1c8b03bf607e8902cbd663961a007d9eb7e2847bd96ce696c/wrapt-2.2.0rc11-cp312-cp312-win_amd64.whl", hash = "sha256:c1f27bb9866a53445fe28dabb5e0770c8a625f00072537f5981afd08f5188e64", size = 80769, upload-time = "2026-04-24T10:16:56.208Z" }, - { url = "https://files.pythonhosted.org/packages/29/91/ecfff0b6dbdd1598b347baacbe6c57f9b4ffc67bf737618baef8cc3be36a/wrapt-2.2.0rc11-cp312-cp312-win_arm64.whl", hash = "sha256:bf0e904769d96b1d68971ae4015771e2ceecf1cbba2dff606468cc312444a258", size = 79038, upload-time = "2026-04-24T10:15:25.801Z" }, - { url = "https://files.pythonhosted.org/packages/53/b8/de8018f7bafa5b550654f3eb564645c7c5e7bf853e0c3f90546a7ec49e54/wrapt-2.2.0rc11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8ca1d5666fc5a26452ff369085b605a8f791532eed00c62af272ea330d636c16", size = 80773, upload-time = "2026-04-24T10:16:06.936Z" }, - { url = "https://files.pythonhosted.org/packages/49/56/2afbe0bdb0f31a6a29292eadd037ebe4d2ad5a9dc51cd375b5e82589d332/wrapt-2.2.0rc11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f759fff25b56481ae16aadb0c91dfcd77ff1e66be6da3bd5664766ed4adde59f", size = 81303, upload-time = "2026-04-24T10:16:23.695Z" }, - { url = "https://files.pythonhosted.org/packages/15/c5/e30fe36a5f5f4a4dbc3bceba9d0fa5271ec8d7639bc59f096bac47dfd198/wrapt-2.2.0rc11-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0d4445e090535c73af539aad56f681d8a0e40bc02d4717d5ea1c39fcbf367bc1", size = 166728, upload-time = "2026-04-24T10:15:27.724Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b0/cae82622e6e834eaa40b21a16918d7cc8bec32550d2cc9aa0d386b8697f5/wrapt-2.2.0rc11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb8690e53d5801bf8a79446dd3898a957f34d2384593daf78dd181fda716319e", size = 166782, upload-time = "2026-04-24T10:17:16.491Z" }, - { url = "https://files.pythonhosted.org/packages/4c/41/20556af3b9f9c605c0eac3432c724135064b6ed4a21b475dd8666c636276/wrapt-2.2.0rc11-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25ef8e95c76c82821342336b1979e11583ff0bca1d4402e9a7f9a9b689d81f65", size = 157843, upload-time = "2026-04-24T10:15:31.307Z" }, - { url = "https://files.pythonhosted.org/packages/15/d2/8711d5c936d8328dbaf650291f59a979ce14ff49b52d8b6aabfa8cfb8acb/wrapt-2.2.0rc11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9d4d055acfe55633ff260d8c268a230ddf6cd408ac5a3bacfaeaa59ca13431cb", size = 165748, upload-time = "2026-04-24T10:17:03.385Z" }, - { url = "https://files.pythonhosted.org/packages/86/fa/f4dc4f9b1c8c8d61b7efbf4d53e1aa6193073199749a796115f2d8f1b0e6/wrapt-2.2.0rc11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4d97d727dcd414391c476dfdfb77ebe45082d9499d8b1cbc1149a96991d5e2b2", size = 156532, upload-time = "2026-04-24T10:16:20.511Z" }, - { url = "https://files.pythonhosted.org/packages/75/4d/4d876bf16b89278b269550aece6520406d58669f0651e6852b3c0293336e/wrapt-2.2.0rc11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9af734e2a6d48ef84e5c6f48e5b18d6e5904447aa9298d1e3457ba9d8865689d", size = 165901, upload-time = "2026-04-24T10:15:53.283Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6a/54c60b18148ca3d017137de3f902d308bdb4ec31f5a1f9465f03afc98724/wrapt-2.2.0rc11-cp313-cp313-win32.whl", hash = "sha256:ce1dbe00d7aad0205d469f85b69f5dc1532e8e85f4989cc1258376c6b98d8246", size = 77763, upload-time = "2026-04-24T10:17:31.486Z" }, - { url = "https://files.pythonhosted.org/packages/a1/8d/9c7e7d01704e8a2b6d49c4ccc641a4dca8f37700f22a888fd3ba5937bc41/wrapt-2.2.0rc11-cp313-cp313-win_amd64.whl", hash = "sha256:abfb2f5f455783a03390a5b020efa7590a0c9a9059cc5b6a6badb5f15dad38c9", size = 80678, upload-time = "2026-04-24T10:17:23.695Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1a/645faa10a61f8d75806fa2031f509fd0720142a1f24cedb7e79bcc97c03a/wrapt-2.2.0rc11-cp313-cp313-win_arm64.whl", hash = "sha256:3b3efc5b5325e7f63982a52246f58b2c3eb0afc360017dad22485cfc7ecc6b40", size = 79036, upload-time = "2026-04-24T10:16:26.444Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fa/806052da337fc153603eb461c6ffd67af4ec4e52adaa60a882af2a0d786c/wrapt-2.2.0rc11-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:923490056004b62079d810a8214457329c4bcf9eecf6e3a3508fd387c0d0ed4c", size = 82706, upload-time = "2026-04-24T10:17:22.198Z" }, - { url = "https://files.pythonhosted.org/packages/16/60/ee692cec34f3b91e904e358dc29b4554d00ef47aef71cad1166a89f3f1d4/wrapt-2.2.0rc11-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:785f93eb4a8a417ab732338708dcf0d2cca3ac5d1df0b749ba9d79a7d9d8c3fa", size = 83277, upload-time = "2026-04-24T10:15:29.471Z" }, - { url = "https://files.pythonhosted.org/packages/51/01/4ea8f9e9098277dce4953f2ac2cfac7e9dff3b48de312685c5b8cb4ab237/wrapt-2.2.0rc11-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f49955a6ad62c2011d2049d4cb80d903aa7fa0c75a4a092a0e12d26f6234d005", size = 203709, upload-time = "2026-04-24T10:17:24.938Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ea/a03697f3b18b5bb07c304ba16027707dcf9beded31c8f0898db0b969c9ba/wrapt-2.2.0rc11-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00fc51db6261d47b97b12d2b377347c15e3ffccc3a18bc1ac2bc296e420088aa", size = 209622, upload-time = "2026-04-24T10:17:06.603Z" }, - { url = "https://files.pythonhosted.org/packages/4b/06/e41ef42c16adb7ad783d03854d6ca90353780d468f21ff9b52a1ffa772d8/wrapt-2.2.0rc11-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8d4cc5fceffc47390dc1ce0acd2163e7d6d1f145ee57d489e7f099d1876c6e2e", size = 194637, upload-time = "2026-04-24T10:15:09.927Z" }, - { url = "https://files.pythonhosted.org/packages/23/8c/099a117f155423dd0be7d90c46b63757fb6e6d62f71950c36fcb966040b4/wrapt-2.2.0rc11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60b7ac5a746713f034d3b0e6c65f215d90f872b170b82efa118df8af31838799", size = 205321, upload-time = "2026-04-24T10:16:31.031Z" }, - { url = "https://files.pythonhosted.org/packages/33/8f/0b652dd807fd25769cc8b19fe82bc39bcf65b225c69be038a92c3d5e7518/wrapt-2.2.0rc11-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:cb85f8211d1bda71cf8b6f9a425e0824573a799c293a6b79e906c33d7fb296ba", size = 192095, upload-time = "2026-04-24T10:16:32.605Z" }, - { url = "https://files.pythonhosted.org/packages/62/61/fb913f197e647fbbc49399c3fbb9addc1f6cee953185da1014bbad6c5c2e/wrapt-2.2.0rc11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:eb3dd184ab1ad3f4b0692a599dc91ebdf2e4a689e8d4a73014b5c7e333c8ee8d", size = 199180, upload-time = "2026-04-24T10:16:08.82Z" }, - { url = "https://files.pythonhosted.org/packages/47/82/7cb3d889191203250d3ab9335fac20c0eb70ea25146d68789d0ff74a7eb1/wrapt-2.2.0rc11-cp313-cp313t-win32.whl", hash = "sha256:08dc26d3d7a6efb21fdd644b91235a509ad9cdaa158a25fb5c3eff0d64e31450", size = 79380, upload-time = "2026-04-24T10:15:11.961Z" }, - { url = "https://files.pythonhosted.org/packages/33/ee/63cbd676d011a78077ad03062c3b32deeac07a52bf61078b4a2964fc006b/wrapt-2.2.0rc11-cp313-cp313t-win_amd64.whl", hash = "sha256:3883b31768f3381c96b8f59c75cf0f3070b457d44e2ed9ed41896f0725df4a35", size = 82963, upload-time = "2026-04-24T10:15:43.899Z" }, - { url = "https://files.pythonhosted.org/packages/5a/7c/dfd8135e5988740eff4e41a87155508db28bc5877c0fdc27942354304d31/wrapt-2.2.0rc11-cp313-cp313t-win_arm64.whl", hash = "sha256:9dd990000f133f2961a5bfee6a4aad07ad075792122df8e797b0854d31373a58", size = 80224, upload-time = "2026-04-24T10:17:14.992Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/fbf6a0f4193b9beef222a14638d176d346532971bc7df499d120538e71ce/wrapt-2.2.0rc11-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6decf7275b26ed3397b4a3beefe2436ebd75e2348c15f75e3a5223e65231a1d7", size = 80817, upload-time = "2026-04-24T10:17:17.818Z" }, - { url = "https://files.pythonhosted.org/packages/af/5c/02ee0ddd25f2e8d7f1b61646858ea48748c08603d38b45192b32c2bc4765/wrapt-2.2.0rc11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:21686c1d2625346c90a6a8abb019ae2e985f77b51d4b28be9290dcbde0036f81", size = 81398, upload-time = "2026-04-24T10:16:41.631Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a6/41ff243e781d127e429f79f2e8ecd907efeb0bb990412b7bb05c945ef57d/wrapt-2.2.0rc11-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5481f1406125cc9cdffd8c054e1ba45213f58a28d62cb5854654bc37dbc1ffb9", size = 166614, upload-time = "2026-04-24T10:16:37.217Z" }, - { url = "https://files.pythonhosted.org/packages/68/28/47ae8e1bfe412762f08b97a824ee7d2e4bb9284951a1e280921fe112c414/wrapt-2.2.0rc11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbc9681f2adaf789cf04688430169969c206c9b67904feba092cea53377f0919", size = 166215, upload-time = "2026-04-24T10:15:05.466Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c0/67b6f568ae1858983c1702f303be4bb009bc551b3a48c2e52161bd60056e/wrapt-2.2.0rc11-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb24cc8134bd03be435e0272c692fbe7450658939291501c3496c65f155c1b7b", size = 157651, upload-time = "2026-04-24T10:15:33.278Z" }, - { url = "https://files.pythonhosted.org/packages/f6/48/88982438be70262037eaca70dd128f03abd9600694d114c8671e8cde4c78/wrapt-2.2.0rc11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:294f8ed73cc4f498150903553f50f582772cc194c72fc7c60382c7de30410ecf", size = 165992, upload-time = "2026-04-24T10:16:18.995Z" }, - { url = "https://files.pythonhosted.org/packages/80/32/fa7f70286cdc235af0239535d8ec5da4c2049c83e0ec2b2d6c44d89231eb/wrapt-2.2.0rc11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8bd9c2b5d8f799aca53a0a1a8f81355447c42b00826f93fc7a1ca20325c2139e", size = 156394, upload-time = "2026-04-24T10:15:35.033Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f7/b58a85a4fd651ad540eda37eedcbe3a4abdc70c1981ea2674eee8b0f005d/wrapt-2.2.0rc11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7c4076d31907715869df3a97366d114e02f909d3e41ce0b1c3b6b00df82a6226", size = 165448, upload-time = "2026-04-24T10:17:37.199Z" }, - { url = "https://files.pythonhosted.org/packages/f1/87/904307947657b2b1cce7304968c69e72fa6195e87435288e970942e8a385/wrapt-2.2.0rc11-cp314-cp314-win32.whl", hash = "sha256:d0fe901e422671d45c09bd1a8a5f36130eeea1711ec10a0c5e017c7af4a4d044", size = 78284, upload-time = "2026-04-24T10:17:19.081Z" }, - { url = "https://files.pythonhosted.org/packages/7f/06/d0de22123f64259518baa385b2e7fc8c5913547cca37072174f4bc2f6f23/wrapt-2.2.0rc11-cp314-cp314-win_amd64.whl", hash = "sha256:8109f72963b6b6e15fa8511be18bbb3a369f5033b444b5b97c853deb813b0553", size = 81086, upload-time = "2026-04-24T10:16:38.819Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b2/44f0e04cadb1f57890235ed2aa57e2519518ccbb1d1bb88bcaf80cc18693/wrapt-2.2.0rc11-cp314-cp314-win_arm64.whl", hash = "sha256:51c87d3285669347383705118347b7f446cdc23cb13cc4b0baed5b04032df106", size = 79516, upload-time = "2026-04-24T10:16:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/7e/b8/015cd6157537d9c80f60783fc6df2240af3b12b382732ab7eeecb46febff/wrapt-2.2.0rc11-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:703b2f8c21d1be1027742ba4f34536f5b5717e34077bb04e09b205eb6c493a3a", size = 82801, upload-time = "2026-04-24T10:15:54.77Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ba/cb228a7c98be16d4920b5230693cadceb3feadbd6e658466dc79f0de0049/wrapt-2.2.0rc11-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bfe526ca947c4d830bb0a18caabc5d1aee52a7714cfe898981434a2e03f1002", size = 83276, upload-time = "2026-04-24T10:16:12.756Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b7/15976c633431310c955c2a935211b734e236136d9f4475e2b5212536dadc/wrapt-2.2.0rc11-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:700978189597d950cf7714fb50923afa5c98f931da804bafbc5b41d83dcbb0a8", size = 203698, upload-time = "2026-04-24T10:15:56.75Z" }, - { url = "https://files.pythonhosted.org/packages/6a/71/45592fa1517ddabb5ddef0331f4938077e3c672e59de5a352341579e4349/wrapt-2.2.0rc11-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78a7447b83cfb007b2b09e7f32131b43a9a662072701fed68cec42a835025214", size = 209628, upload-time = "2026-04-24T10:16:43.389Z" }, - { url = "https://files.pythonhosted.org/packages/95/b5/86f46e4a1c7cfbe456984be10593b5a871aa69e853b3ef5640021e3d4f0d/wrapt-2.2.0rc11-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4853b4ed7c806985bc366a5b3600b83a7c7c4609f8ea5599df45ddc94a32db94", size = 194677, upload-time = "2026-04-24T10:17:09.417Z" }, - { url = "https://files.pythonhosted.org/packages/1b/61/28184784b6ea7b17e6bd5b3253055665c907feb1fbacc7633908b9e82738/wrapt-2.2.0rc11-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:77cc036f79eaf72861329bab07f180b9ca192e3b17d17f3466b88b4f04372b33", size = 205291, upload-time = "2026-04-24T10:15:39.848Z" }, - { url = "https://files.pythonhosted.org/packages/af/c7/8afd82fc060d1e958a958c0be505cf983da0f7949b05a55c9cc8c1847490/wrapt-2.2.0rc11-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd6dc7339f6eb2b3e5556125d202bb2172ea8c9ebe68f0abbca67e6e1661a3c8", size = 192127, upload-time = "2026-04-24T10:16:05.053Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/18ae952432ffec22ae9e1f37cec4570fb3f321c83d05527813dae31fcc26/wrapt-2.2.0rc11-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b5623b1f2495cae98baadb2f4e4f37323128050c43b7e994047cf3618a5227af", size = 199157, upload-time = "2026-04-24T10:15:16.586Z" }, - { url = "https://files.pythonhosted.org/packages/f8/94/291693ae8e6706a08ed5e9368d883f14da8aab408bfa88117f4945c0db7c/wrapt-2.2.0rc11-cp314-cp314t-win32.whl", hash = "sha256:e6f4e23aadd29401414ae9c8ee12189cf93ceac63814bb7c2e54e38d42b1da79", size = 80146, upload-time = "2026-04-24T10:16:10.093Z" }, - { url = "https://files.pythonhosted.org/packages/40/08/cee79e056b80f510bf30a86b2f44649a2aa07e0331e77afa226df18ab9d6/wrapt-2.2.0rc11-cp314-cp314t-win_amd64.whl", hash = "sha256:4c03de92788b3b9f7d862212d93c8b8f19328a97f1371e9c8560ce6178b21d48", size = 83770, upload-time = "2026-04-24T10:17:32.965Z" }, - { url = "https://files.pythonhosted.org/packages/3e/53/8f4348643e9b3fef1efede571b0f3aa282846e73b1e2bd16289d9cbba180/wrapt-2.2.0rc11-cp314-cp314t-win_arm64.whl", hash = "sha256:be23d203b7cbbf35147efae0db17feffee59d540138989cd3838c233505db8a3", size = 80650, upload-time = "2026-04-24T10:16:11.574Z" }, - { url = "https://files.pythonhosted.org/packages/42/d9/bee80519aaf88101996d653050e6d78aa3a63d87d6f735fd63955414f7c9/wrapt-2.2.0rc11-py3-none-any.whl", hash = "sha256:48a0ea119e937ec94452b4b6a4301bb6a435f18262298e141cc49b7e495df782", size = 60936, upload-time = "2026-04-24T10:16:48.108Z" }, +version = "1.17.4rc1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/e0/c6c3e66c6ca371728de87b44102b61f3fdacc03c8b0b1e4ac5f30d71c5ce/wrapt-1.17.4rc1.tar.gz", hash = "sha256:19c0363cb46f42cf5536c7b9d9c921cc1ae24e55fe4d45c3a19315e9f2aa8964", size = 55653, upload-time = "2026-03-06T05:27:09.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/09/f4b0c4c5098ee0a4e89542d259f2ce2a15124efb43cbd0aae442d284d4f8/wrapt-1.17.4rc1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ed597307c29facdfe47eb5521123b121d189a2bbae3e34dfaf10a1f8ebb9bc1", size = 39033, upload-time = "2026-03-06T05:28:00.852Z" }, + { url = "https://files.pythonhosted.org/packages/c6/61/779692b7228a9e2f430edc2137737821f7e249f73be30d589ddc3c92532c/wrapt-1.17.4rc1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:97a6d6b40c2347fc6ea5017c715a4ac0a29716ae17b70060f24c1ca22757289a", size = 39296, upload-time = "2026-03-06T05:27:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/6cf5d4cae58fd19a0b89f977aaac957795930123c917d44536d6a04c0745/wrapt-1.17.4rc1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c5e9e219bd65d89356da8af2168fb23e3480736949ffad617d6d73a16039a5dc", size = 88141, upload-time = "2026-03-06T05:27:44.037Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/eba8b87158819858fcd2a6d1c80276a22085292866554bb82faa731e042f/wrapt-1.17.4rc1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:647fae8af1ac1789023ba267fd84522096db737a522597b53fbf3fe2b45482db", size = 88256, upload-time = "2026-03-06T05:27:40.31Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b5/d547471fd5eb77280157f70698ae5e91913d6fecc1bc2eb9a90ccb7e27f1/wrapt-1.17.4rc1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d8ed3e2538fbacd8b62462c58676aabba38ca8e9e8ad6c11ed94ec0db926e29", size = 84248, upload-time = "2026-03-06T05:28:15.363Z" }, + { url = "https://files.pythonhosted.org/packages/46/ce/cc8e75f1bcc230031037940cf33e0361fca3229296ebf706459598ef00da/wrapt-1.17.4rc1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae3dabc85555f5a8330b324d466d577f3cc60669150fe8e381719b5b680113b0", size = 87208, upload-time = "2026-03-06T05:28:23.392Z" }, + { url = "https://files.pythonhosted.org/packages/4a/62/f879b4f4c320049708ba5e02fda0756dd93c78e8831ef881323074594404/wrapt-1.17.4rc1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e27719a9b75517191cbe23e5f54dd410f39076d6e8369c259a1b990c6ac924f4", size = 83645, upload-time = "2026-03-06T05:21:23.051Z" }, + { url = "https://files.pythonhosted.org/packages/35/c8/ff8bf340cf45aaf300ac864772085a1fac27a265b6565081da91294b0176/wrapt-1.17.4rc1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c53fcd7cf09a223eaab9f425dda6e38929f4534112df0def102ffa5ef9da6086", size = 87828, upload-time = "2026-03-06T05:21:20.84Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ed/e3c750db3c19eae7bcd2506291d85595127637b37e180be72cf65951f779/wrapt-1.17.4rc1-cp312-cp312-win32.whl", hash = "sha256:7062f45cc386554e94521da25cf1b89b65e72ff5e1b62c2c6735a5c4dfe61b19", size = 36803, upload-time = "2026-03-06T05:21:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/58/07/a58ebce46e2258989c4147b26bcf4926aeebc9aea2a21f581a3a6a4ba3ac/wrapt-1.17.4rc1-cp312-cp312-win_amd64.whl", hash = "sha256:aaf599f8535cbc8c7c016763e72486cfeae933382f23b2c1b632952bee4f11ae", size = 38968, upload-time = "2026-03-06T05:21:12.013Z" }, + { url = "https://files.pythonhosted.org/packages/c3/52/f3a464bc629690b8d0551be8187fdbce57e26337eabc11acda67c2bf18bd/wrapt-1.17.4rc1-cp312-cp312-win_arm64.whl", hash = "sha256:488c903c475c54ef062f6a2c0c49dffd608d501bc8d05e061ff19eb794f31fb2", size = 36940, upload-time = "2026-03-06T05:26:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/94/c9/bfb0840b9d1a3e9478c9d6bd1b5e2fb82fdca7c046bc10e8c44f9273cd46/wrapt-1.17.4rc1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d1a4c658bea05c1b22ae374f74c25b400535f3dccbf795b121153d5628216f0", size = 39037, upload-time = "2026-03-06T05:28:18.986Z" }, + { url = "https://files.pythonhosted.org/packages/41/82/1e234ad6b64cd705557a0a682dbdce499db082a1932f9c95f200ed0843da/wrapt-1.17.4rc1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491d11b84ac47568ee88777304c42d047d33307ec82162235d7e8261ee983eaa", size = 39295, upload-time = "2026-03-06T05:28:26.945Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d9/2143f5825ef49046b376a2d9136621a7aa66a9e93ccc82b162d9e79ab678/wrapt-1.17.4rc1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16426870299de6370b93760a50ae5bc813548f4666e6e515dcce3ec7601b9c59", size = 88175, upload-time = "2026-03-06T05:27:25.018Z" }, + { url = "https://files.pythonhosted.org/packages/f3/dd/8add4d24770a2e960f2bb8cb062a83f880a6aa91664b01d6de1e62917e45/wrapt-1.17.4rc1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a35158b0bf2c2d2033eba3c56832e803d73658dc4e92f14f1ea4c92ab0dfaafe", size = 88320, upload-time = "2026-03-06T05:27:37.675Z" }, + { url = "https://files.pythonhosted.org/packages/9c/34/c47fd4837b07b9f8ae8cfe749ea0d6fe5ea506c2d324850f3067f5f66ca2/wrapt-1.17.4rc1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a33bff65de96bc32f7f1df1492c2808068070ed0f42f1fcef2b47846f6a6a03a", size = 84302, upload-time = "2026-03-06T05:21:09.738Z" }, + { url = "https://files.pythonhosted.org/packages/9d/b0/20542954e5929383f55da30d4b9a47764866a2d253d8bea0a5d366ea1e7c/wrapt-1.17.4rc1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:931dae558932c8ba8e4de77ce92ed505fe5a8fd9dab66a2cbbc9d5d3a3a32bb4", size = 87210, upload-time = "2026-03-06T05:27:39.024Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ff/37a5295d7f01b270186191035f67142f3052882210f673b9a62f82fcfc9f/wrapt-1.17.4rc1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ea3a5f62f5980031aaf6e28074cb17cea8df06cb828bcd2882d525f7ccc2f9", size = 83709, upload-time = "2026-03-06T05:27:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/05/ff/11f668fba8ab6436c3a0167d0dc2aacb1c9fca675c1268a83d9106457b0b/wrapt-1.17.4rc1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b302dc5e126057f74b82223c3b19a41dfeead10292667be1538985ef75034f3b", size = 87866, upload-time = "2026-03-06T05:27:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/41/b7e49896146dd95fc8e9ecda84ede824c5d34105dd84aa5f4e108a0c137a/wrapt-1.17.4rc1-cp313-cp313-win32.whl", hash = "sha256:27bf0d37ebcd4a43e8369eaf60dd9ea45f30933a921453f61bd6476ffe39bbfb", size = 36810, upload-time = "2026-03-06T05:28:25.862Z" }, + { url = "https://files.pythonhosted.org/packages/86/8d/ba014ec122b07b6441eb9ed341514045a4c79677186623733be460c379b3/wrapt-1.17.4rc1-cp313-cp313-win_amd64.whl", hash = "sha256:22e85eab852e7182c41acef5f9d95d5d63a1b115910951fb38feccf67b514818", size = 38977, upload-time = "2026-03-06T05:26:55.473Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ef/6561940fba308d086f5967827c63bce7dbf8c54717bc33c7f523f0018400/wrapt-1.17.4rc1-cp313-cp313-win_arm64.whl", hash = "sha256:5be27331b6eae2317350c4adee1cf92edc0866cd7db726f574f10c8db227c134", size = 36944, upload-time = "2026-03-06T05:27:00.79Z" }, + { url = "https://files.pythonhosted.org/packages/58/3e/1ab40e5f926d0650fd5b7e23cebcdd4eab6bed961ac6e7ed5307638ddd27/wrapt-1.17.4rc1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:474a45ee2dfa6bb8c1a2a63fbc91c53da010caece85464a334fdb9aabafb6ecb", size = 40438, upload-time = "2026-03-06T05:27:58.196Z" }, + { url = "https://files.pythonhosted.org/packages/af/26/8d288da55259592a9aff160af4192db56799a74d3389ce032f54c8c8b74c/wrapt-1.17.4rc1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dcf7b65ca203123c8613ae609441812b53ae047495e72b0dc423e5d31510128", size = 40586, upload-time = "2026-03-06T05:27:03.329Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d4/cd7b78cb59ea4d348a77906dfac3d30ed1c598732d9ee3cd8edcf7762bca/wrapt-1.17.4rc1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1713dac1faf01465058481dd07f7632847ca8867e77347527788aff0bdb32d8a", size = 108627, upload-time = "2026-03-06T05:27:28.884Z" }, + { url = "https://files.pythonhosted.org/packages/0b/26/6ae3790d46b56010f01dd74a207af7aebb7357b95487e222d1a6ad912f84/wrapt-1.17.4rc1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:094bef74a0ef4c04416775a4f1965b2a29847d6aafa935229c1bf9d18f1d8c58", size = 113179, upload-time = "2026-03-06T05:27:48.59Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0e/c0b0b05de9ebf705ca8daa1e86c20a244ce0862f08faf1b23784d3abf766/wrapt-1.17.4rc1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e1f828a32b4e71b6349a00a0a3bcc9e41413e0005160fb70601b83cb171ce6e", size = 103238, upload-time = "2026-03-06T05:21:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/ec/33/b2cd9f6b86bf322cb1711c6070b9efa6b28a8e8c063f56d165b30c8d1668/wrapt-1.17.4rc1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e24a05dae0ba49ce5f490bffc4e369a5770663c789c0bc862de8ac235b18394d", size = 110742, upload-time = "2026-03-06T05:27:46.881Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f4/b9709eea1e0087c8ccb1c7a38076a76ec3eb6f0555f74c3a65eaadf5c987/wrapt-1.17.4rc1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7d00e2453975a6519cbdde4812234ab0183860011aae2316acbad46f3b8e84e9", size = 102364, upload-time = "2026-03-06T05:27:51.288Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/bd4a00aef4d4b1a7eff25456b2f9c15de8ec9a3f4ccf98f0acdb2c48c879/wrapt-1.17.4rc1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89b5fe920975e4e63099aeb194b51ad0ad84b45995dada353aa1e5a551462fd0", size = 107013, upload-time = "2026-03-06T05:28:03.308Z" }, + { url = "https://files.pythonhosted.org/packages/86/73/aedee294890bde90b262b21156c20eb36450ee812a20384ea5df9ba49bbd/wrapt-1.17.4rc1-cp313-cp313t-win32.whl", hash = "sha256:c94efd8ca87b9333590b6ee0384a0863ad92b54646232396c3c8043b0d115d49", size = 38129, upload-time = "2026-03-06T05:27:12.074Z" }, + { url = "https://files.pythonhosted.org/packages/96/17/dbf146893d31705872d2e515cd2ef70e01e305aa441a1736cdeee856deb9/wrapt-1.17.4rc1-cp313-cp313t-win_amd64.whl", hash = "sha256:db3ea738ffd95b88a5874ed6c7d26ffad1b482a5b8036e7b4b667926d3d5d728", size = 40751, upload-time = "2026-03-06T05:27:18.843Z" }, + { url = "https://files.pythonhosted.org/packages/70/a1/2bafa54d3621ca0c8a0b7cd78150d6239e83553f8f2bf8e6fc17286bac34/wrapt-1.17.4rc1-cp313-cp313t-win_arm64.whl", hash = "sha256:d8f67707f553821691228bf3596bf60cf83e112c230ca4ebfae759feed20cf57", size = 38262, upload-time = "2026-03-06T05:26:54.324Z" }, + { url = "https://files.pythonhosted.org/packages/84/e2/203c4a94a4f2cb5bd1b2180261f213b6ecf386839d9c4a7b03b187e1d973/wrapt-1.17.4rc1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4384529d0f82bcdebec1d01f7b714b31ea34ee1b43a8399df5ed0db443bf6551", size = 39210, upload-time = "2026-03-06T05:21:13.2Z" }, + { url = "https://files.pythonhosted.org/packages/b9/de/0f3940df4cf001cc79cfd321c7e7856e6cdeac4c53b8292b4d318884a9be/wrapt-1.17.4rc1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d665e1f4bdeb551c55a56fe076f3da2aa4acea9b5108723adf4347b9af17bb70", size = 39339, upload-time = "2026-03-06T05:28:28.027Z" }, + { url = "https://files.pythonhosted.org/packages/28/87/1b13a950ad90919078951cadc8c8418241f55f6355bc1b64420072453d2f/wrapt-1.17.4rc1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95be0b13dcde68f73921026c66b4bb464a299683365a7243b5db49f220e5463f", size = 87262, upload-time = "2026-03-06T05:27:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/00/a9/c3015e3929b715ae2737eb332dc5e056bb0a3a450d26dca962dc93da8a32/wrapt-1.17.4rc1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7e86063ed1d5b46e2c6ac7c3c8c9bb1b47e47d3ceb804a93f566d1294810505", size = 88061, upload-time = "2026-03-06T05:27:33.243Z" }, + { url = "https://files.pythonhosted.org/packages/15/8f/83d676e926c2c6390e6019aacb3f598c929426d67d1d97d3ed26536a0ac9/wrapt-1.17.4rc1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c710707166eed80e37242d754a204f4c07b8f3ab8024b07d583f48024d260a05", size = 84543, upload-time = "2026-03-06T05:28:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/87/8d/f48862187bcee1d7d0a6c2c8cf4830ecd9e06bf0d770e6efbd2a78b70dad/wrapt-1.17.4rc1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c85cf9d6017e5188697a5947dd76f29ba1c56707ea612173b1b1ee1bc27b9601", size = 87050, upload-time = "2026-03-06T05:27:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/1e3c265902f02b3c1644568be86ddc3cf0d76552723ae71b7ca11e10bdc3/wrapt-1.17.4rc1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:44edeaf45e144c2de1102427530790c32eeb0084451f7816a58d744d077e0b3c", size = 83965, upload-time = "2026-03-06T05:27:08.164Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4c/24a7c0fa058212cb53a7f582c9631b1b9ce9d5a81400095c745a1cb7a4be/wrapt-1.17.4rc1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:201acefeff4fc6d497f411595c46f79eb91e562fa4883847db8148474a1e3d80", size = 86958, upload-time = "2026-03-06T05:28:24.737Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/445569dc31ee7a23c199afae532a41cc2f446d434d288e7544b1a38fbd19/wrapt-1.17.4rc1-cp314-cp314-win32.whl", hash = "sha256:73016054d0e32a65fa5da708e839be3036c786416adca00a0444aec5837b1b83", size = 37276, upload-time = "2026-03-06T05:28:21.776Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/1636a670886dec6c59fa60a8112fc3fd56c194b23b07106dbee465af73c2/wrapt-1.17.4rc1-cp314-cp314-win_amd64.whl", hash = "sha256:66b0485668cff7bfac0eaccccb3a991dba3f0d5205d6bc5a9c69aa120b2b6ccf", size = 39405, upload-time = "2026-03-06T05:26:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5e/9f820a1d60ea579b048a8486c319918fdf06b83cc37f67f8dd4c53b80df6/wrapt-1.17.4rc1-cp314-cp314-win_arm64.whl", hash = "sha256:2712e6caad2a5032d6496612eeca5cdb65fadd6da55c5f931d556ac656e3ebdd", size = 37367, upload-time = "2026-03-06T05:27:23.446Z" }, + { url = "https://files.pythonhosted.org/packages/14/92/617f98da4517f2bf2a63b1a929f5bec029292d6bd31c7fd79ee25d54635e/wrapt-1.17.4rc1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3102bbdc650a7e8fd8672e51c6d204688fc75257e2d3c6a12172a8e05c2ab0cd", size = 40565, upload-time = "2026-03-06T05:26:50.47Z" }, + { url = "https://files.pythonhosted.org/packages/6b/80/8c4444c471d90f9cfe1b453e5bf605fccadb2d3399d2ed60ed3240c188b3/wrapt-1.17.4rc1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a3ef8f9aad3593f3b00527da3815e15941caf169c51da5da18e64d1949da3f29", size = 40585, upload-time = "2026-03-06T05:21:14.419Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fb/c3938d7fef6ce445d32e5a757268adc4e5c298d1985dff95c535e1ceca38/wrapt-1.17.4rc1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:033b67f5cc44d992221617ea6be6f12d8857b90a5d0901738f4f6c92498d3298", size = 108671, upload-time = "2026-03-06T05:28:16.715Z" }, + { url = "https://files.pythonhosted.org/packages/ad/54/d5ae3c39c871ff63c973848558c1657fa09cf84c19e5242e25f57e8b251a/wrapt-1.17.4rc1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b3c400c7c7b6346e9d3d22f036443ff033fa924d472715d127f169e8f9e137", size = 113193, upload-time = "2026-03-06T05:27:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/18/c0/37f69e1231e8cfd3e642ff24f002cd71cbe477fca2abe6ec43978426f09a/wrapt-1.17.4rc1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b3a9ed0f966b6a199e251800f5ee895bb41694ad1bb92f19446cbb90e68cdec", size = 103256, upload-time = "2026-03-06T05:27:52.645Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5b/71f5f63bb3c4bfa909ae320ebcf290250cd86207d54cdffc3b12c1a57b8a/wrapt-1.17.4rc1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:076702de22f5df07bfaeb67ac750aabe2167fd703ed60ac8e2edb42a082119e8", size = 110756, upload-time = "2026-03-06T05:26:59.375Z" }, + { url = "https://files.pythonhosted.org/packages/fe/52/6ef9887520e0038cacb97bfd4375a83e3cf947d82a11e4017af2a98647cb/wrapt-1.17.4rc1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:1374e2051eff90875b3331dc5930209807db9e03ba863c2a9009ab7ba77daa7c", size = 102369, upload-time = "2026-03-06T05:26:52.912Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/670237dcee12fb293cb4674f93db112806783a33cc8cc18fa64214c12614/wrapt-1.17.4rc1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a02b14dfc3ded8f1be82d824628ccda63ac37d1833c8328adf7a6b019f6a230", size = 107045, upload-time = "2026-03-06T05:27:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/1a/15/2ecc4112171d195ff1c4f0baf7d345ca5f0ec464381bc7024857b3db47d5/wrapt-1.17.4rc1-cp314-cp314t-win32.whl", hash = "sha256:2bdf836e6c8e8f26c85716c08a0063309a2d9362e090b499f32fc4de8f2c651d", size = 38809, upload-time = "2026-03-06T05:21:15.397Z" }, + { url = "https://files.pythonhosted.org/packages/d7/45/81fec744e8c88f6255a5ccc317997a01b1a08fa925b211e2078fa8bfbddf/wrapt-1.17.4rc1-cp314-cp314t-win_amd64.whl", hash = "sha256:f75df0a7f1dab354cd092ee9c466efb3556f87ecf103683cecc0f7488e9dbf77", size = 41427, upload-time = "2026-03-06T05:28:17.885Z" }, + { url = "https://files.pythonhosted.org/packages/3d/72/d6ecf86cb5f3574a55fd2ba58c6eca447bee90a8757f1f32fba4b14ff9d5/wrapt-1.17.4rc1-cp314-cp314t-win_arm64.whl", hash = "sha256:3e2f5e602d656b53118bfdc9d5d94b840069f1753923e48726f0bc02dd65deb8", size = 38531, upload-time = "2026-03-06T05:27:57.157Z" }, + { url = "https://files.pythonhosted.org/packages/29/b2/367cc462b6ad84bfb7a93b00f5c4b01c7bc880a0e7ce36c1a3900eee153a/wrapt-1.17.4rc1-py3-none-any.whl", hash = "sha256:9cc3fb27bc5f564895c967b9b06dd2b799ee107b33a7f8ad8b8346b5d6b35b60", size = 23719, upload-time = "2026-03-06T05:27:55.715Z" }, ] [[package]] From 27261c7ecc81fdbc71e2a77f1a065e0c296ac197 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 17:50:25 +0530 Subject: [PATCH 12/50] increase the tokens --- src/processor/src/libs/base/orchestrator_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index fbcb39e2..9203772f 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -183,7 +183,7 @@ async def create_agents( builder .with_temperature(0.0) .with_response_format(ManagerSelectionResponse) - .with_max_tokens(4_000) + .with_max_tokens(10_000) .with_tools(agent_info.tools) # for checking file existence ) elif agent_info.agent_name == "ResultGenerator": From 49eb5cdf33d9a3b4c70a71762ecf1c9e389a6c5d Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Thu, 11 Jun 2026 21:19:20 +0530 Subject: [PATCH 13/50] Upgrade agent-framework to 1.3.0 and azure-ai-projects to 2.1.0 - Update agent-framework from 1.0.0b260107 to 1.3.0 in pyproject.toml - Update azure-ai-projects from 1.0.0b12 to 2.1.0 in requirements.txt - Migrate ChatAgent to Agent (client=, default_options=ChatOptions) - Migrate agent_framework.azure to agent_framework.openai module paths - Migrate ChatMessage to Message with Content.from_text() - Migrate Role enum to string literals - Migrate AgentRunContext to AgentContext - Migrate WorkflowBuilder to new API (start_executor=, add_chain) - Migrate event handling from isinstance checks to WorkflowEvent.type - Migrate GroupChatBuilder to agent_framework.orchestrations module - Migrate ContextProvider to before_run/after_run interface - Remove ToolProtocol (use Any), AgentProtocol (use SupportsAgentRun) - Define ManagerSelectionResponse locally (removed from framework) - Update MCP tool files for Agent import - Update all unit tests for new APIs (812 tests passing) - Update docs/ProcessFrameworkGuide.md with new WorkflowBuilder example - Update docs/LocalDevelopmentSetup.md prerelease note - Regenerate uv.lock Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/LocalDevelopmentSetup.md | 2 +- docs/ProcessFrameworkGuide.md | 20 +- infra/vscode_web/codeSample.py | 19 +- infra/vscode_web/requirements.txt | 2 +- src/processor/pyproject.toml | 2 +- .../src/libs/agent_framework/agent_builder.py | 264 ++++++++++----- .../agent_framework/agent_framework_helper.py | 88 ++--- .../src/libs/agent_framework/agent_info.py | 5 +- .../agent_framework/agent_speaking_capture.py | 4 +- .../azure_openai_response_retry.py | 159 +-------- .../agent_framework/groupchat_orchestrator.py | 93 +++--- .../src/libs/agent_framework/middlewares.py | 18 +- .../shared_memory_context_provider.py | 70 ++-- .../src/libs/base/orchestrator_base.py | 15 +- .../src/libs/mcp_server/MCPBlobIOTool.py | 14 +- .../src/libs/mcp_server/MCPDatetimeTool.py | 12 +- .../src/libs/mcp_server/MCPMicrosoftDocs.py | 8 +- .../orchestration/analysis_orchestrator.py | 6 +- .../yaml_convert_orchestrator.py | 5 +- .../orchestration/design_orchestrator.py | 5 +- .../documentation_orchestrator.py | 5 +- .../src/steps/migration_processor.py | 98 +++--- .../agent_framework/test_agent_builder.py | 25 +- .../test_agent_framework_helper.py | 75 +++-- .../test_groupchat_orchestrator_internals.py | 89 +++-- .../test_input_observer_middleware.py | 6 +- .../test_middlewares_extras.py | 8 +- .../test_shared_memory_context_provider.py | 129 +++++--- .../steps/test_migration_processor_run.py | 95 +++--- src/processor/uv.lock | 305 +++++++++++++++--- 30 files changed, 900 insertions(+), 746 deletions(-) diff --git a/docs/LocalDevelopmentSetup.md b/docs/LocalDevelopmentSetup.md index dc82ed3f..7df64705 100644 --- a/docs/LocalDevelopmentSetup.md +++ b/docs/LocalDevelopmentSetup.md @@ -337,7 +337,7 @@ py -3.12 -m uv venv .venv py -3.12 -m uv sync --prerelease=allow ``` -> **⚠️ Important**: This repo currently depends on a prerelease/dev version of Microsoft Agent Framework. Always run `uv sync --prerelease=allow` (or `py -3.12 -m uv sync --prerelease=allow` on Windows) after creating the virtual environment to install all required dependencies. Missing dependencies will cause runtime errors like `ModuleNotFoundError: No module named 'pydantic'` or DNS resolution failures. +> **⚠️ Important**: Always run `uv sync --prerelease=allow` (or `py -3.12 -m uv sync --prerelease=allow` on Windows) after creating the virtual environment to install all required dependencies. The `--prerelease=allow` flag is needed because some transitive dependencies may still use pre-release versions. Missing dependencies will cause runtime errors like `ModuleNotFoundError: No module named 'pydantic'` or DNS resolution failures. ### 5.4. Run the Processor diff --git a/docs/ProcessFrameworkGuide.md b/docs/ProcessFrameworkGuide.md index 575d4a1c..8732ec49 100644 --- a/docs/ProcessFrameworkGuide.md +++ b/docs/ProcessFrameworkGuide.md @@ -118,23 +118,21 @@ Inside each step, the orchestrator can use multi-agent patterns (maker-checker l - Implementation: [src/processor/src/steps/migration_processor.py](../src/processor/src/steps/migration_processor.py) - The processor creates a workflow with `WorkflowBuilder`. -- It registers four executors, sets the start executor, and defines edges. +- It instantiates four executors, passes the start executor to `WorkflowBuilder`, and chains them with `add_chain`. Example from the repo (simplified): ```python from agent_framework import WorkflowBuilder +analysis_exec = AnalysisExecutor(id="analysis", app_context=app_context) +design_exec = DesignExecutor(id="design", app_context=app_context) +yaml_exec = YamlConvertExecutor(id="yaml", app_context=app_context) +docs_exec = DocumentationExecutor(id="documentation", app_context=app_context) + workflow = ( - WorkflowBuilder() - .register_executor(lambda: AnalysisExecutor(id="analysis", app_context=app_context), name="analysis") - .register_executor(lambda: DesignExecutor(id="design", app_context=app_context), name="design") - .register_executor(lambda: YamlConvertExecutor(id="yaml", app_context=app_context), name="yaml") - .register_executor(lambda: DocumentationExecutor(id="documentation", app_context=app_context), name="documentation") - .set_start_executor("analysis") - .add_edge("analysis", "design") - .add_edge("design", "yaml") - .add_edge("yaml", "documentation") + WorkflowBuilder(start_executor=analysis_exec) + .add_chain([analysis_exec, design_exec, yaml_exec, docs_exec]) .build() ) ``` @@ -355,7 +353,7 @@ To run processor unit tests locally (example): ```bash cd src/processor -uv run --prerelease=allow python -m pytest src/processor/src/tests/unit -v +uv run python -m pytest src/tests/unit -v ``` ## Extending the pipeline diff --git a/infra/vscode_web/codeSample.py b/infra/vscode_web/codeSample.py index 37224009..288a5084 100644 --- a/infra/vscode_web/codeSample.py +++ b/infra/vscode_web/codeSample.py @@ -1,3 +1,4 @@ +from azure.ai.agents.models import ListSortOrder from azure.ai.projects import AIProjectClient from azure.identity import DefaultAzureCredential @@ -7,19 +8,25 @@ agent = project_client.agents.get_agent("<%= agentId %>") -thread = project_client.agents.create_thread() +thread = project_client.agents.threads.create() print(f"Created thread, ID: {thread.id}") -message = project_client.agents.create_message( +message = project_client.agents.messages.create( thread_id=thread.id, role="user", content="<%= userMessage %>" ) -run = project_client.agents.create_and_process_run( +run = project_client.agents.runs.create_and_process( thread_id=thread.id, agent_id=agent.id) -messages = project_client.agents.list_messages(thread_id=thread.id) -for text_message in messages.text_messages: - print(text_message.as_dict()) +if run.status == "failed": + print(f"Run failed: {run.last_error}") +else: + messages = project_client.agents.messages.list( + thread_id=thread.id, order=ListSortOrder.ASCENDING) + + for message in messages: + if message.text_messages: + print(f"{message.role}: {message.text_messages[-1].text.value}") diff --git a/infra/vscode_web/requirements.txt b/infra/vscode_web/requirements.txt index 18d6803e..d7ff98e4 100644 --- a/infra/vscode_web/requirements.txt +++ b/infra/vscode_web/requirements.txt @@ -1,3 +1,3 @@ -azure-ai-projects==1.0.0b12 +azure-ai-projects==2.1.0 azure-identity==1.20.0 ansible-core~=2.17.0 \ No newline at end of file diff --git a/src/processor/pyproject.toml b/src/processor/pyproject.toml index b6c2c2aa..d72e9c5f 100644 --- a/src/processor/pyproject.toml +++ b/src/processor/pyproject.toml @@ -5,7 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "agent-framework==1.0.0b260107", + "agent-framework==1.3.0", "aiohttp==3.13.5", "art==6.5", "azure-ai-agents==1.2.0b5", diff --git a/src/processor/src/libs/agent_framework/agent_builder.py b/src/processor/src/libs/agent_framework/agent_builder.py index 8b9c629e..888e2dae 100644 --- a/src/processor/src/libs/agent_framework/agent_builder.py +++ b/src/processor/src/libs/agent_framework/agent_builder.py @@ -1,26 +1,68 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -"""Fluent builder for constructing ChatAgent instances with chainable configuration.""" +"""Fluent builder for constructing Agent instances with chainable configuration.""" +import logging from collections.abc import Callable, MutableMapping, Sequence from typing import Any, Literal from agent_framework import ( - AggregateContextProvider, - ChatAgent, - ChatClientProtocol, - ChatMessageStoreProtocol, + Agent, + AgentMiddleware, + ChatMiddleware, + ChatOptions, ContextProvider, - Middleware, + HistoryProvider, + SupportsChatGetResponse, ToolMode, - ToolProtocol, ) from pydantic import BaseModel from libs.agent_framework.agent_info import AgentInfo from utils.credential_util import get_bearer_token_provider +logger = logging.getLogger(__name__) + +# Reasoning models that do not support custom temperature/top_p values. +_REASONING_MODEL_PREFIXES = ("o1", "o3", "o4", "gpt-5") + + +def is_reasoning_model(model_name: str) -> bool: + """Check if a model is a reasoning model based on its deployment name.""" + name = model_name.lower() + return any(name.startswith(prefix) for prefix in _REASONING_MODEL_PREFIXES) + + +def resolve_model_name(client: Any, model_id: str | None = None) -> str | None: + """Extract model/deployment name from the client or explicit model_id.""" + if model_id: + return model_id + for attr in ("model", "_model", "model_id"): + val = getattr(client, attr, None) + if isinstance(val, str): + return val + return None + + +def _strip_unsupported_reasoning_params( + options: dict[str, Any], model_name: str | None +) -> dict[str, Any]: + """Remove temperature and top_p for reasoning models that don't support them.""" + if model_name and is_reasoning_model(model_name): + removed = [] + for param in ("temperature", "top_p"): + if param in options: + del options[param] + removed.append(param) + if removed: + logger.info( + "Stripped unsupported params %s for reasoning model '%s'", + removed, + model_name, + ) + return options + class AgentBuilder: """Fluent builder for creating ChatAgent instances with a chainable API. @@ -59,7 +101,7 @@ class AgentBuilder: ) """ - def __init__(self, chat_client: ChatClientProtocol): + def __init__(self, chat_client: SupportsChatGetResponse): """Initialize the builder with a chat client. Args: @@ -70,14 +112,15 @@ def __init__(self, chat_client: ChatClientProtocol): self._id: str | None = None self._name: str | None = None self._description: str | None = None - self._chat_message_store_factory: ( - Callable[[], ChatMessageStoreProtocol] | None - ) = None + self._chat_message_store_factory: Callable[[], HistoryProvider] | None = None self._conversation_id: str | None = None - self._context_providers: ( - ContextProvider | list[ContextProvider] | AggregateContextProvider | None + self._context_providers: ContextProvider | list[ContextProvider] | None = None + self._middleware: ( + AgentMiddleware + | ChatMiddleware + | list[AgentMiddleware | ChatMiddleware] + | None ) = None - self._middleware: Middleware | list[Middleware] | None = None self._frequency_penalty: float | None = None self._logit_bias: dict[str | int, float] | None = None self._max_tokens: int | None = None @@ -93,10 +136,10 @@ def __init__(self, chat_client: ChatClientProtocol): ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None ) = "auto" self._tools: ( - ToolProtocol + Any | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[Any | Callable[..., Any] | MutableMapping[str, Any]] | None ) = None self._top_p: float | None = None @@ -178,10 +221,10 @@ def with_max_tokens(self, max_tokens: int) -> "AgentBuilder": def with_tools( self, - tools: ToolProtocol + tools: Any | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]], + | Sequence[Any | Callable[..., Any] | MutableMapping[str, Any]], ) -> "AgentBuilder": """Set the tools available to the agent. @@ -210,7 +253,10 @@ def with_tool_choice( return self def with_middleware( - self, middleware: Middleware | list[Middleware] + self, + middleware: AgentMiddleware + | ChatMiddleware + | list[AgentMiddleware | ChatMiddleware], ) -> "AgentBuilder": """Set middleware for request/response processing. @@ -225,9 +271,7 @@ def with_middleware( def with_context_providers( self, - context_providers: ContextProvider - | list[ContextProvider] - | AggregateContextProvider, + context_providers: ContextProvider | list[ContextProvider], ) -> "AgentBuilder": """Set context providers for additional conversation context. @@ -385,7 +429,7 @@ def with_store(self, store: bool) -> "AgentBuilder": return self def with_message_store_factory( - self, factory: Callable[[], ChatMessageStoreProtocol] + self, factory: Callable[[], HistoryProvider] ) -> "AgentBuilder": """Set the message store factory. @@ -422,11 +466,11 @@ def with_kwargs(self, **kwargs: Any) -> "AgentBuilder": self._kwargs.update(kwargs) return self - def build(self) -> ChatAgent: - """Build and return the configured ChatAgent. + def build(self) -> Agent: + """Build and return the configured Agent. Returns: - ChatAgent: Configured agent instance ready for use + Agent: Configured agent instance ready for use Example: .. code-block:: python @@ -442,32 +486,55 @@ def build(self) -> ChatAgent: async with agent: response = await agent.run("Hello!") """ - return ChatAgent( - chat_client=self._chat_client, + options: dict[str, Any] = {} + if self._frequency_penalty is not None: + options["frequency_penalty"] = self._frequency_penalty + if self._logit_bias is not None: + options["logit_bias"] = self._logit_bias + if self._max_tokens is not None: + options["max_tokens"] = self._max_tokens + if self._metadata is not None: + options["metadata"] = self._metadata + if self._model_id is not None: + options["model"] = self._model_id + if self._presence_penalty is not None: + options["presence_penalty"] = self._presence_penalty + if self._response_format is not None: + options["response_format"] = self._response_format + if self._seed is not None: + options["seed"] = self._seed + if self._stop is not None: + options["stop"] = self._stop + if self._store is not None: + options["store"] = self._store + if self._temperature is not None: + options["temperature"] = self._temperature + if self._tool_choice is not None: + options["tool_choice"] = self._tool_choice + if self._top_p is not None: + options["top_p"] = self._top_p + if self._user is not None: + options["user"] = self._user + if self._conversation_id is not None: + options["conversation_id"] = self._conversation_id + if self._additional_chat_options: + options.update(self._additional_chat_options) + + model_name = resolve_model_name(self._chat_client, self._model_id) + _strip_unsupported_reasoning_params(options, model_name) + + default_options: ChatOptions | None = ChatOptions(**options) if options else None + + return Agent( + client=self._chat_client, instructions=self._instructions, id=self._id, name=self._name, description=self._description, - chat_message_store_factory=self._chat_message_store_factory, - conversation_id=self._conversation_id, + tools=self._tools, + default_options=default_options, context_providers=self._context_providers, middleware=self._middleware, - frequency_penalty=self._frequency_penalty, - logit_bias=self._logit_bias, - max_tokens=self._max_tokens, - metadata=self._metadata, - model_id=self._model_id, - presence_penalty=self._presence_penalty, - response_format=self._response_format, - seed=self._seed, - stop=self._stop, - store=self._store, - temperature=self._temperature, - tool_choice=self._tool_choice, - tools=self._tools, - top_p=self._top_p, - user=self._user, - additional_chat_options=self._additional_chat_options, **self._kwargs, ) @@ -477,14 +544,12 @@ def create_agent_by_agentinfo( agent_info: AgentInfo, *, id: str | None = None, - chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] - | None = None, + chat_message_store_factory: Callable[[], HistoryProvider] | None = None, conversation_id: str | None = None, - context_providers: ContextProvider - | list[ContextProvider] - | AggregateContextProvider - | None = None, - middleware: Middleware | list[Middleware] | None = None, + context_providers: ContextProvider | list[ContextProvider] | None = None, + middleware: ( + AgentMiddleware | ChatMiddleware | list[AgentMiddleware | ChatMiddleware] | None + ) = None, frequency_penalty: float | None = None, logit_bias: dict[str | int, float] | None = None, max_tokens: int | None = None, @@ -500,16 +565,16 @@ def create_agent_by_agentinfo( | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto", - tools: ToolProtocol + tools: Any | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[Any | Callable[..., Any] | MutableMapping[str, Any]] | None = None, top_p: float | None = None, user: str | None = None, additional_chat_options: dict[str, Any] | None = None, **kwargs: Any, - ) -> ChatAgent: + ) -> Agent: """Create an agent using AgentInfo configuration with full parameter support. This method creates a chat client from the service configuration and then @@ -611,20 +676,18 @@ def create_agent_by_agentinfo( @staticmethod def create_agent( - chat_client: ChatClientProtocol, + chat_client: SupportsChatGetResponse, instructions: str | None = None, *, id: str | None = None, name: str | None = None, description: str | None = None, - chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] - | None = None, + chat_message_store_factory: Callable[[], HistoryProvider] | None = None, conversation_id: str | None = None, - context_providers: ContextProvider - | list[ContextProvider] - | AggregateContextProvider - | None = None, - middleware: Middleware | list[Middleware] | None = None, + context_providers: ContextProvider | list[ContextProvider] | None = None, + middleware: ( + AgentMiddleware | ChatMiddleware | list[AgentMiddleware | ChatMiddleware] | None + ) = None, frequency_penalty: float | None = None, logit_bias: dict[str | int, float] | None = None, max_tokens: int | None = None, @@ -640,16 +703,16 @@ def create_agent( | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto", - tools: ToolProtocol + tools: Any | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[Any | Callable[..., Any] | MutableMapping[str, Any]] | None = None, top_p: float | None = None, user: str | None = None, additional_chat_options: dict[str, Any] | None = None, **kwargs: Any, - ) -> ChatAgent: + ) -> Agent: """Create a Chat Client Agent. Factory method that creates a ChatAgent instance with the specified configuration. @@ -751,7 +814,7 @@ def create_agent( temperature=0.7, max_tokens=500, additional_chat_options={ - "reasoning": {"effort": "high", "summary": "concise"} + "reasoning_effort": "high" }, # OpenAI-specific reasoning options ) @@ -761,34 +824,57 @@ def create_agent( Note: When the agent has MCP tools or needs proper resource cleanup, use it with - ``async with`` to ensure proper initialization and cleanup via the ChatAgent's + ``async with`` to ensure proper initialization and cleanup via the Agent's async context manager protocol. """ - return ChatAgent( - chat_client=chat_client, + options: dict[str, Any] = {} + if frequency_penalty is not None: + options["frequency_penalty"] = frequency_penalty + if logit_bias is not None: + options["logit_bias"] = logit_bias + if max_tokens is not None: + options["max_tokens"] = max_tokens + if metadata is not None: + options["metadata"] = metadata + if model_id is not None: + options["model"] = model_id + if presence_penalty is not None: + options["presence_penalty"] = presence_penalty + if response_format is not None: + options["response_format"] = response_format + if seed is not None: + options["seed"] = seed + if stop is not None: + options["stop"] = stop + if store is not None: + options["store"] = store + if temperature is not None: + options["temperature"] = temperature + if tool_choice is not None: + options["tool_choice"] = tool_choice + if top_p is not None: + options["top_p"] = top_p + if user is not None: + options["user"] = user + if conversation_id is not None: + options["conversation_id"] = conversation_id + if additional_chat_options: + options.update(additional_chat_options) + + model_name = resolve_model_name(chat_client, model_id) + _strip_unsupported_reasoning_params(options, model_name) + + default_options: ChatOptions | None = ChatOptions(**options) if options else None + + return Agent( + client=chat_client, instructions=instructions, id=id, name=name, description=description, - chat_message_store_factory=chat_message_store_factory, - conversation_id=conversation_id, + tools=tools, + default_options=default_options, context_providers=context_providers, middleware=middleware, - frequency_penalty=frequency_penalty, - logit_bias=logit_bias, - max_tokens=max_tokens, - metadata=metadata, - model_id=model_id, - presence_penalty=presence_penalty, - response_format=response_format, - seed=seed, - stop=stop, - store=store, - temperature=temperature, - tool_choice=tool_choice, - tools=tools, - top_p=top_p, - user=user, - additional_chat_options=additional_chat_options, **kwargs, ) diff --git a/src/processor/src/libs/agent_framework/agent_framework_helper.py b/src/processor/src/libs/agent_framework/agent_framework_helper.py index 61da842a..b1bf6ba8 100644 --- a/src/processor/src/libs/agent_framework/agent_framework_helper.py +++ b/src/processor/src/libs/agent_framework/agent_framework_helper.py @@ -27,11 +27,9 @@ ) if TYPE_CHECKING: - from agent_framework.azure import ( - AzureAIAgentClient, - AzureOpenAIAssistantsClient, - AzureOpenAIChatClient, - AzureOpenAIResponsesClient, + from agent_framework.openai import ( + OpenAIChatClient, + OpenAIChatCompletionClient, ) @@ -147,7 +145,7 @@ def create_client( env_file_path: str | None = None, env_file_encoding: str | None = None, instruction_role: str | None = None, - ) -> "AzureOpenAIChatClient": + ) -> "OpenAIChatCompletionClient": pass @overload @@ -171,7 +169,7 @@ def create_client( async_client: object | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, - ) -> "AzureOpenAIAssistantsClient": + ) -> Any: pass @overload @@ -193,7 +191,7 @@ def create_client( env_file_path: str | None = None, env_file_encoding: str | None = None, instruction_role: str | None = None, - ) -> "AzureOpenAIResponsesClient": + ) -> "OpenAIChatClient": pass @overload @@ -233,7 +231,7 @@ def create_client( async_credential: object | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, - ) -> "AzureAIAgentClient": + ) -> Any: pass @staticmethod @@ -366,18 +364,15 @@ def create_client( "OpenAIResponsesClient is not implemented in this context." ) elif client_type == ClientType.AzureOpenAIChatCompletion: - from agent_framework.azure import AzureOpenAIChatClient + from agent_framework.openai import OpenAIChatCompletionClient - return AzureOpenAIChatClient( + return OpenAIChatCompletionClient( + model=deployment_name, api_key=api_key, - deployment_name=deployment_name, - endpoint=endpoint, + azure_endpoint=endpoint, base_url=base_url, api_version=api_version, - ad_token=ad_token, - ad_token_provider=ad_token_provider, - token_endpoint=token_endpoint, - credential=credential, + credential=credential or ad_token_provider, default_headers=default_headers, async_client=async_client, env_file_path=env_file_path, @@ -385,39 +380,20 @@ def create_client( instruction_role=instruction_role, ) elif client_type == ClientType.AzureOpenAIAssistant: - from agent_framework.azure import AzureOpenAIAssistantsClient - - return AzureOpenAIAssistantsClient( - deployment_name=deployment_name, - assistant_id=assistant_id, - assistant_name=assistant_name, - thread_id=thread_id, - api_key=api_key, - endpoint=endpoint, - base_url=base_url, - api_version=api_version, - ad_token=ad_token, - ad_token_provider=ad_token_provider, - token_endpoint=token_endpoint, - credential=credential, - default_headers=default_headers, - async_client=async_client, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, + raise NotImplementedError( + "AzureOpenAIAssistantsClient has been removed in agent-framework 1.3.0. " + "Use OpenAIChatClient with built-in tools instead." ) elif client_type == ClientType.AzureOpenAIResponse: - from agent_framework.azure import AzureOpenAIResponsesClient + from agent_framework.openai import OpenAIChatClient - return AzureOpenAIResponsesClient( + return OpenAIChatClient( + model=deployment_name, api_key=api_key, - deployment_name=deployment_name, - endpoint=endpoint, + azure_endpoint=endpoint, base_url=base_url, api_version=api_version, - ad_token=ad_token, - ad_token_provider=ad_token_provider, - token_endpoint=token_endpoint, - credential=credential, + credential=credential or ad_token_provider, default_headers=default_headers, async_client=async_client, env_file_path=env_file_path, @@ -426,15 +402,12 @@ def create_client( ) elif client_type == ClientType.AzureOpenAIResponseWithRetry: return AzureOpenAIResponseClientWithRetry( + model=deployment_name, api_key=api_key, - deployment_name=deployment_name, - endpoint=endpoint, + azure_endpoint=endpoint, base_url=base_url, api_version=api_version, - ad_token=ad_token, - ad_token_provider=ad_token_provider, - token_endpoint=token_endpoint, - credential=credential, + credential=credential or ad_token_provider, default_headers=default_headers, async_client=async_client, env_file_path=env_file_path, @@ -443,18 +416,9 @@ def create_client( retry_config=retry_config, ) elif client_type == ClientType.AzureOpenAIAgent: - from agent_framework.azure import AzureAIAgentClient - - return AzureAIAgentClient( - project_client=project_client, - agent_id=agent_id, - agent_name=agent_name, - thread_id=thread_id, - project_endpoint=project_endpoint, - model_deployment_name=model_deployment_name, - async_credential=async_credential, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, + raise NotImplementedError( + "AzureAIAgentClient has been removed in agent-framework 1.3.0. " + "Use FoundryChatClient from agent_framework.foundry instead." ) else: raise ValueError(f"Unsupported agent type: {client_type}") diff --git a/src/processor/src/libs/agent_framework/agent_info.py b/src/processor/src/libs/agent_framework/agent_info.py index 8eb18de3..fbd6cafe 100644 --- a/src/processor/src/libs/agent_framework/agent_info.py +++ b/src/processor/src/libs/agent_framework/agent_info.py @@ -4,7 +4,6 @@ """Pydantic model describing an agent participant with Jinja2 template rendering.""" from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import ToolProtocol from jinja2 import Template from openai import BaseModel from pydantic import Field @@ -20,10 +19,10 @@ class AgentInfo(BaseModel): agent_instruction: str | None = Field(default=None) agent_framework_helper: AgentFrameworkHelper | None = Field(default=None) tools: ( - ToolProtocol + Any | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[Any | Callable[..., Any] | MutableMapping[str, Any]] | None ) = Field(default=None) diff --git a/src/processor/src/libs/agent_framework/agent_speaking_capture.py b/src/processor/src/libs/agent_framework/agent_speaking_capture.py index 8243d755..fe5cc0ff 100644 --- a/src/processor/src/libs/agent_framework/agent_speaking_capture.py +++ b/src/processor/src/libs/agent_framework/agent_speaking_capture.py @@ -5,7 +5,7 @@ from datetime import datetime from typing import Any, Callable, Optional -from agent_framework import AgentRunContext, AgentMiddleware +from agent_framework import AgentContext, AgentMiddleware class AgentSpeakingCaptureMiddleware(AgentMiddleware): @@ -72,7 +72,7 @@ def __init__( str, list[str] ] = {} # Buffer for streaming responses - async def process(self, context: AgentRunContext, next): + async def process(self, context: AgentContext, next): """Process the agent invocation and capture the response. Args: diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 42f6422e..3fbd0e8b 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -10,9 +10,9 @@ import os import random from dataclasses import dataclass -from typing import Any, AsyncIterable, MutableSequence +from typing import Any, MutableSequence -from agent_framework.azure import AzureOpenAIResponsesClient +from agent_framework.openai import OpenAIChatClient from tenacity import ( AsyncRetrying, retry_if_exception, @@ -513,11 +513,11 @@ def __call__(self, retry_state) -> float: raise RuntimeError("Retry loop exhausted unexpectedly") -class AzureOpenAIResponseClientWithRetry(AzureOpenAIResponsesClient): +class AzureOpenAIResponseClientWithRetry(OpenAIChatClient): """Azure OpenAI Responses client with 429 retry at the request boundary. Retry is centralized in the client layer (not in orchestrators) by retrying the - underlying Responses calls made by `OpenAIBaseResponsesClient`. + underlying Responses calls made by `OpenAIChatClient`. """ def __init__( @@ -531,7 +531,7 @@ def __init__( self._context_trim_config = ContextTrimConfig.from_env() async def _inner_get_response( - self, *, messages: MutableSequence[Any], chat_options: Any, **kwargs: Any + self, *, messages: MutableSequence[Any], options: Any = None, **kwargs: Any ) -> Any: parent_inner_get_response = super( AzureOpenAIResponseClientWithRetry, self @@ -558,7 +558,7 @@ async def _inner_get_response( try: return await _retry_call( lambda: parent_inner_get_response( - messages=effective_messages, chat_options=chat_options, **kwargs + messages=effective_messages, options=options, **kwargs ), config=self._retry_config, ) @@ -606,153 +606,8 @@ async def _inner_get_response( await asyncio.sleep(trim_delay) return await _retry_call( lambda: parent_inner_get_response( - messages=trimmed, chat_options=chat_options, **kwargs + messages=trimmed, options=options, **kwargs ), config=self._retry_config, ) - async def _inner_get_streaming_response( - self, *, messages: MutableSequence[Any], chat_options: Any, **kwargs: Any - ) -> AsyncIterable[Any]: - # Conservative retry: only retries failures before the first yielded update. - attempts = self._retry_config.max_retries + 1 - - effective_messages: MutableSequence[Any] | list[Any] = messages - if self._context_trim_config.enabled: - approx_chars = sum(len(_estimate_message_text(m)) for m in messages) - if ( - self._context_trim_config.max_total_chars > 0 - and approx_chars > self._context_trim_config.max_total_chars - ): - effective_messages = _trim_messages( - messages, cfg=self._context_trim_config - ) - logger.warning( - "[AOAI_CTX_TRIM] pre-trimmed streaming request messages: approx_chars=%s -> %s; count=%s -> %s", - approx_chars, - sum(len(_estimate_message_text(m)) for m in effective_messages), - len(messages), - len(effective_messages), - ) - - for attempt_index in range(attempts): - stream = super( - AzureOpenAIResponseClientWithRetry, self - )._inner_get_streaming_response( - messages=effective_messages, chat_options=chat_options, **kwargs - ) - - iterator = stream.__aiter__() - try: - first = await iterator.__anext__() - - async def _tail(): - yield first - async for item in iterator: - yield item - - async for item in _tail(): - yield item - return - except StopAsyncIteration: - return - except Exception as e: - close = getattr(stream, "aclose", None) - if callable(close): - try: - await close() - except Exception: - logger.debug("Best-effort close of response stream failed", exc_info=True) - - # Progressive retry for context-length failures. - if ( - self._context_trim_config.enabled - and self._context_trim_config.retry_on_context_error - and _looks_like_context_length(e) - ): - # Make trimming progressively more aggressive on each retry - # GPT-5.1: 272K input tokens ≈ 800K chars. Scale down from 600K default. - scale = attempt_index + 1 - aggressive_cfg = ContextTrimConfig( - enabled=True, - max_total_chars=max( - 30_000, - self._context_trim_config.max_total_chars - scale * 100_000, - ), - max_message_chars=max( - 2_000, - self._context_trim_config.max_message_chars - scale * 8_000, - ), - keep_last_messages=max( - 4, - self._context_trim_config.keep_last_messages - scale * 8, - ), - keep_head_chars=max( - 500, - self._context_trim_config.keep_head_chars - scale * 3_000, - ), - keep_tail_chars=max( - 500, - self._context_trim_config.keep_tail_chars - scale * 1_000, - ), - keep_system_messages=True, - retry_on_context_error=True, - ) - trimmed = _trim_messages(effective_messages, cfg=aggressive_cfg) - logger.warning( - "[AOAI_CTX_TRIM_STREAM] retrying after context-length error (attempt %s); count=%s -> %s, budget=%s", - attempt_index + 1, - len(effective_messages), - len(trimmed), - aggressive_cfg.max_total_chars, - ) - effective_messages = trimmed - if attempt_index >= attempts - 1: - # No more retries available. - raise - - # Cool down before retrying — immediate retries after trimming - # tend to trigger 429s because the API hasn't recovered yet. - trim_delay = self._retry_config.base_delay_seconds * ( - 2**attempt_index - ) - trim_delay = min(trim_delay, self._retry_config.max_delay_seconds) - logger.info( - "[AOAI_CTX_TRIM_STREAM] sleeping %ss before retry", - round(trim_delay, 1), - ) - await asyncio.sleep(trim_delay) - continue - - if not _looks_like_rate_limit(e) or attempt_index >= attempts - 1: - if _looks_like_rate_limit(e): - logger.warning( - "[AOAI_RETRY_STREAM] giving up after %s/%s attempts; error=%s", - attempt_index + 1, - attempts, - _format_exc_brief(e) - if isinstance(e, BaseException) - else str(e), - ) - raise - - retry_after = _try_get_retry_after_seconds(e) - if retry_after is not None and retry_after >= 0: - delay = retry_after - else: - delay = self._retry_config.base_delay_seconds * (2**attempt_index) - delay = min(delay, self._retry_config.max_delay_seconds) - delay = delay + random.uniform(0.0, 0.25 * max(delay, 0.1)) - - status = getattr(e, "status_code", None) or getattr(e, "status", None) - logger.warning( - "[AOAI_RETRY_STREAM] attempt %s/%s; sleeping=%ss; retry_after=%s; status=%s; error=%s", - attempt_index + 1, - attempts, - round(float(delay), 3), - None if retry_after is None else round(float(retry_after), 3), - status, - _format_exc_brief(e) if isinstance(e, BaseException) else str(e), - ) - - await asyncio.sleep(delay) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 5cb63938..5c3fc9ab 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -22,17 +22,16 @@ from typing import Any, Awaitable, Callable, Generic, Mapping, Sequence, TypeVar from agent_framework import ( - AgentProtocol, - AgentRunUpdateEvent, - ChatAgent, - ChatMessage, + Agent, + AgentResponseUpdate, + ChatOptions, Executor, - GroupChatBuilder, - ManagerSelectionResponse, - Role, + Message, + SupportsAgentRun, Workflow, - WorkflowOutputEvent, + WorkflowEvent, ) +from agent_framework.orchestrations import GroupChatBuilder from mem0 import AsyncMemory from pydantic import BaseModel, ValidationError @@ -44,6 +43,17 @@ TOutput = TypeVar("TOutput", bound=BaseModel) # Output must be Pydantic model +class ManagerSelectionResponse(BaseModel): + """Coordinator selection payload parsed from JSON output.""" + + selected_participant: str | None = None + instruction: str | None = None + finish: bool | None = None + final_message: str | None = None + + model_config = {"extra": "allow"} + + @dataclass class AgentResponse: """Represents a single agent's response during workflow execution""" @@ -87,7 +97,7 @@ class OrchestrationResult(Generic[TOutput]): """Final workflow execution result with generic output type""" success: bool - conversation: list[ChatMessage] + conversation: list[Message] agent_responses: list[AgentResponse] tool_usage: dict[str, list[dict[str, Any]]] result: TOutput | None = None @@ -188,8 +198,8 @@ def __init__( self, name: str, process_id: str, - participants: Mapping[str, AgentProtocol | Executor] - | Sequence[AgentProtocol | Executor], + participants: Mapping[str, SupportsAgentRun | Executor] + | Sequence[SupportsAgentRun | Executor], memory_client: AsyncMemory, coordinator_name: str = "Coordinator", max_rounds: int = 100, @@ -225,7 +235,7 @@ def __init__( self.result_format = result_output_format # Runtime state - self.agents: dict[str, ChatAgent] = participants + self.agents: dict[str, Agent] = participants self.agent_tool_usage: dict[str, list[dict[str, Any]]] = {} self.agent_responses: list[AgentResponse] = [] self._initialized: bool = False @@ -338,7 +348,7 @@ def get_result_generator_name(self) -> str: """ return "ResultGenerator" - def _validate_sign_offs(self, conversation: list[ChatMessage]) -> tuple[bool, str]: + def _validate_sign_offs(self, conversation: list[Message]) -> tuple[bool, str]: """ Validate that all required reviewers have SIGN-OFF: PASS. @@ -475,7 +485,7 @@ async def run_stream( self._tool_call_emitted.clear() self._tool_call_recorded.clear() self._tool_call_index.clear() - self._conversation: list[ChatMessage] = [] # Track conversation during workflow + self._conversation: list[Message] = [] # Track conversation during workflow try: # Ensure initialized @@ -489,9 +499,9 @@ async def run_stream( group_chat_workflow = await self._build_groupchat() # Execute with streaming - conversation: list[ChatMessage] = [] + conversation: list[Message] = [] - async for event in group_chat_workflow.run_stream(task_prompt): + async for event in group_chat_workflow.run(task_prompt, stream=True): # Enforce wall-clock timeout if configured. if self.max_seconds is not None: elapsed = (datetime.now() - start_time).total_seconds() @@ -503,7 +513,7 @@ async def run_stream( termination_type="hard_timeout", ) - if isinstance(event, AgentRunUpdateEvent): + if isinstance(event, AgentResponseUpdate): await self._handle_agent_update( event, stream_callback=on_agent_response_stream, @@ -525,7 +535,8 @@ async def run_stream( # If the Coordinator requested finish=true, stop immediately. if self._termination_requested: break - elif isinstance(event, WorkflowOutputEvent): + elif event.type == "output": + event: WorkflowEvent # Complete last agent's response before finishing if self._last_executor_id and self._current_agent_response: await self._complete_agent_response( @@ -542,8 +553,8 @@ async def run_stream( self._conversation = conversation # Update instance variable # Backfill tool usage from the final conversation (more reliable than streaming updates) - # AgentRunUpdateEvent may stream text only; tool calls are represented as FunctionCallContent - # items inside ChatMessage.contents. + # AgentResponseUpdate may stream text only; tool calls are represented as FunctionCallContent + # items inside Message.contents. self._backfill_tool_usage_from_conversation(conversation) # Post-workflow analysis (optional) @@ -642,7 +653,7 @@ async def run_stream( async def _handle_agent_update( self, - event: AgentRunUpdateEvent, + event: AgentResponseUpdate, stream_callback: AgentResponseStreamCallback | None = None, callback: AgentResponseCallback | None = None, ) -> None: @@ -655,7 +666,7 @@ async def _handle_agent_update( 3. Trigger callback with complete response 4. Handle tool calls separately from text streaming """ - agent_name = self._normalize_executor_id(event.executor_id) + agent_name = self._normalize_executor_id(event.agent_id or "") await self._start_agent_if_needed(agent_name, stream_callback, callback) self._append_text_chunk(event) await self._process_tool_calls(event, agent_name, stream_callback) @@ -705,24 +716,23 @@ async def _start_agent_if_needed( logger.info(f"\n[AGENT] {agent_name}:", extra={"agent_name": agent_name}) - def _append_text_chunk(self, event: AgentRunUpdateEvent) -> None: + def _append_text_chunk(self, event: AgentResponseUpdate) -> None: """Append streamed text chunks to the current agent buffer.""" - if not hasattr(event.data, "text") or not event.data.text: + text_chunk = getattr(event, "text", None) + if not text_chunk: return - text_obj = event.data.text - text_chunk = getattr(text_obj, "text", text_obj) if isinstance(text_chunk, str) and text_chunk: self._current_agent_response.append(text_chunk) async def _process_tool_calls( self, - event: AgentRunUpdateEvent, + event: AgentResponseUpdate, agent_name: str, stream_callback: AgentResponseStreamCallback | None, ) -> None: """Process tool-call contents: buffer/parse args, record once, emit once.""" - tool_calls = self._extract_function_calls(getattr(event.data, "contents", None)) + tool_calls = self._extract_function_calls(event.contents) if not tool_calls: return @@ -884,7 +894,7 @@ def _extract_function_calls(self, contents: Any) -> list[dict[str, Any]]: return calls def _backfill_tool_usage_from_conversation( - self, conversation: list[ChatMessage] + self, conversation: list[Message] ) -> None: """Populate `agent_tool_usage` from final conversation messages. @@ -894,7 +904,7 @@ def _backfill_tool_usage_from_conversation( for msg in conversation: try: role = getattr(msg, "role", None) - if role != Role.ASSISTANT: + if role != "assistant": continue agent_name = getattr(msg, "author_name", None) or "assistant" @@ -1114,15 +1124,16 @@ async def _build_groupchat(self) -> Workflow: ] return ( - GroupChatBuilder() - .set_manager(coordinator) - .participants(participants) + GroupChatBuilder( + orchestrator_agent=coordinator, + participants=participants, + ) .build() ) async def _generate_final_result( self, - conversation: list[ChatMessage], + conversation: list[Message], result_format: type[TOutput], result_generator_name: str, ) -> TOutput: @@ -1141,7 +1152,7 @@ async def _generate_final_result( result = await result_generator.run( final_conversation, - response_format=result_format, + options=ChatOptions(response_format=result_format), ) text = result.messages[-1].text @@ -1174,7 +1185,7 @@ async def _generate_final_result( ) retry_result = await result_generator.run( retry_conversation, - response_format=result_format, + options=ChatOptions(response_format=result_format), ) retry_text = retry_result.messages[-1].text retry_json_payload = self._extract_first_json_payload(retry_text) @@ -1220,7 +1231,7 @@ def _truncate_text( def _build_result_generator_conversation( self, - conversation: Iterable[ChatMessage], + conversation: Iterable[Message], *, exclude_authors: set[str] | None, max_messages: int, @@ -1228,7 +1239,7 @@ def _build_result_generator_conversation( max_chars_per_message: int, keep_head_chars: int, keep_tail_chars: int, - ) -> list[ChatMessage]: + ) -> list[Message]: """Build a size-bounded conversation slice for the ResultGenerator. The raw conversation can contain extremely large tool outputs or repeated @@ -1241,7 +1252,7 @@ def _build_result_generator_conversation( """ exclude = {a.lower() for a in (exclude_authors or set())} - selected: list[ChatMessage] = [] + selected: list[Message] = [] seen_fingerprints: set[tuple[str | None, str, str]] = set() total_chars = 0 @@ -1296,9 +1307,9 @@ def _build_result_generator_conversation( # Preserve role + author_name so downstream can attribute sign-offs. selected.append( - ChatMessage( + Message( role=role, - text=truncated, + contents=[truncated], author_name=author, ) ) diff --git a/src/processor/src/libs/agent_framework/middlewares.py b/src/processor/src/libs/agent_framework/middlewares.py index a24f5b00..573ab0c2 100644 --- a/src/processor/src/libs/agent_framework/middlewares.py +++ b/src/processor/src/libs/agent_framework/middlewares.py @@ -8,13 +8,13 @@ from agent_framework import ( AgentMiddleware, - AgentRunContext, + AgentContext, ChatContext, - ChatMessage, ChatMiddleware, + Content, FunctionInvocationContext, FunctionMiddleware, - Role, + Message, ) @@ -23,8 +23,8 @@ class DebuggingMiddleware(AgentMiddleware): async def process( self, - context: AgentRunContext, - next: Callable[[AgentRunContext], Awaitable[None]], + context: AgentContext, + next: Callable[[AgentContext], Awaitable[None]], ) -> None: """Run-level debugging middleware for troubleshooting specific runs.""" print("[Debug] Debug mode enabled for this run") @@ -136,16 +136,16 @@ async def process( for i, message in enumerate(context.messages): content = message.text if message.text else str(message.contents) - print(f" Message {i + 1} ({message.role.value}): {content}") + print(f" Message {i + 1} ({message.role}): {content}") print(f"[InputObserverMiddleware] Total messages: {len(context.messages)}") # Modify user messages by creating new messages with enhanced text - modified_messages: list[ChatMessage] = [] + modified_messages: list[Message] = [] modified_count = 0 for message in context.messages: - if message.role == Role.USER and message.text: + if message.role == "user" and message.text: original_text = message.text updated_text = original_text @@ -155,7 +155,7 @@ async def process( f"[InputObserverMiddleware] Updated: '{original_text}' -> '{updated_text}'" ) - modified_message = ChatMessage(role=message.role, text=updated_text) + modified_message = Message(role=message.role, contents=[Content.from_text(updated_text)]) modified_messages.append(modified_message) modified_count += 1 else: diff --git a/src/processor/src/libs/agent_framework/shared_memory_context_provider.py b/src/processor/src/libs/agent_framework/shared_memory_context_provider.py index a143a88e..687c4419 100644 --- a/src/processor/src/libs/agent_framework/shared_memory_context_provider.py +++ b/src/processor/src/libs/agent_framework/shared_memory_context_provider.py @@ -18,7 +18,7 @@ from collections.abc import MutableSequence, Sequence from typing import TYPE_CHECKING -from agent_framework import ChatMessage, Context, ContextProvider +from agent_framework import AgentSession, ContextProvider, Message, SessionContext, SupportsAgentRun if TYPE_CHECKING: from libs.agent_framework.qdrant_memory_store import QdrantMemoryStore @@ -49,6 +49,11 @@ class SharedMemoryContextProvider(ContextProvider): redundant embedding calls for intermediate turns) """ + DEFAULT_CONTEXT_PROMPT = ( + "The following are relevant memories from previous migration steps. " + "Use them as additional context when formulating your response:" + ) + def __init__( self, memory_store: QdrantMemoryStore, @@ -66,6 +71,7 @@ def __init__( top_k: Number of relevant memories to retrieve per turn. score_threshold: Minimum similarity score for memory retrieval. """ + super().__init__(source_id=f"shared_memory_{agent_name}_{step}") self._memory_store = memory_store self._agent_name = agent_name self._step = step @@ -85,11 +91,14 @@ def __init__( break self._prior_steps = _STEP_ORDER[:step_idx] if step_idx else [] - async def invoking( + async def before_run( self, - messages: ChatMessage | MutableSequence[ChatMessage], - **kwargs, - ) -> Context: + *, + agent: SupportsAgentRun, + session: AgentSession, + context: SessionContext, + state: dict, + ) -> None: """Called before the agent's LLM call. Injects relevant shared memories. Only searches memories from PREVIOUS steps. Within the current step, @@ -97,12 +106,12 @@ async def invoking( """ # Skip if this is the first step (no prior memories exist) if not self._prior_steps: - return Context() + return # Extract query from the most recent messages - query = self._extract_query(messages) + query = self._extract_query(context.input_messages) if not query: - return Context() + return try: memories = await self._memory_store.search( @@ -116,15 +125,15 @@ async def invoking( self._agent_name, e, ) - return Context() + return if not memories: - return Context() + return # Format memories into context instructions formatted = self._format_memories(memories) if not formatted: - return Context() + return instructions = f"{self.DEFAULT_CONTEXT_PROMPT}\n\n{formatted}" @@ -136,14 +145,17 @@ async def invoking( len(instructions), ) - return Context(instructions=instructions) + if context.instructions is None: + context.instructions = [] + context.instructions.append(instructions) - async def invoked( + async def after_run( self, - request_messages: ChatMessage | Sequence[ChatMessage], - response_messages: ChatMessage | Sequence[ChatMessage] | None = None, - invoke_exception: Exception | None = None, - **kwargs, + *, + agent: SupportsAgentRun, + session: AgentSession, + context: SessionContext, + state: dict, ) -> None: """Called after the agent's LLM response. Buffers the response for storage. @@ -152,17 +164,19 @@ async def invoked( This means only the agent's last response per step gets stored, which is the most complete and useful summary. """ - if invoke_exception is not None: + # Extract text from response messages + response = context.response + if response is None: logger.debug( - "[MEMORY] invoked() skipped for %s — exception: %s", + "[MEMORY] after_run() skipped for %s — no response", self._agent_name, - invoke_exception, ) return - if response_messages is None: + response_messages = getattr(response, "messages", None) + if not response_messages: logger.debug( - "[MEMORY] invoked() skipped for %s — no response_messages", + "[MEMORY] after_run() skipped for %s — no response_messages", self._agent_name, ) return @@ -171,14 +185,14 @@ async def invoked( content = self._extract_text(response_messages) if not content or len(content) < MIN_CONTENT_LENGTH_TO_STORE: logger.debug( - "[MEMORY] invoked() skipped for %s — content too short (%d chars)", + "[MEMORY] after_run() skipped for %s — content too short (%d chars)", self._agent_name, len(content) if content else 0, ) return logger.info( - "[MEMORY] invoked() buffering for %s (step=%s, %d chars)", + "[MEMORY] after_run() buffering for %s (step=%s, %d chars)", self._agent_name, self._step, len(content), @@ -249,7 +263,7 @@ async def _flush_memory(self) -> None: ) def _extract_query( - self, messages: ChatMessage | MutableSequence[ChatMessage] + self, messages: Message | MutableSequence[Message] ) -> str: """Extract a search query from the input messages. @@ -292,8 +306,8 @@ def _format_memories(self, memories: list) -> str: return "\n".join(lines) @staticmethod - def _get_text(message: ChatMessage) -> str: - """Extract text content from a ChatMessage.""" + def _get_text(message: Message) -> str: + """Extract text content from a Message.""" if hasattr(message, "text") and message.text: return message.text if hasattr(message, "content"): @@ -302,7 +316,7 @@ def _get_text(message: ChatMessage) -> str: @staticmethod def _extract_text( - messages: ChatMessage | Sequence[ChatMessage], + messages: Message | Sequence[Message], ) -> str: """Extract text content from response message(s).""" if not isinstance(messages, (list, Sequence)) or isinstance(messages, str): diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index 9203772f..a4570c04 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -9,7 +9,7 @@ from abc import abstractmethod from typing import Any, Callable, Generic, MutableMapping, Sequence, TypeVar -from agent_framework import ChatAgent, ManagerSelectionResponse, ToolProtocol +from agent_framework import Agent from libs.agent_framework.agent_builder import AgentBuilder from libs.agent_framework.agent_framework_helper import ClientType @@ -18,6 +18,7 @@ from libs.agent_framework.groupchat_orchestrator import ( AgentResponse, AgentResponseStream, + ManagerSelectionResponse, OrchestrationResult, ) from libs.agent_framework.qdrant_memory_store import QdrantMemoryStore @@ -60,10 +61,10 @@ def is_console_summarization_enabled(self) -> bool: async def initialize(self, process_id: str): self.mcp_tools: ( - ToolProtocol + Any | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[Any | Callable[..., Any] | MutableMapping[str, Any]] ) = await self.prepare_mcp_tools() self.agentinfos = await self.prepare_agent_infos() @@ -130,10 +131,10 @@ async def execute( async def prepare_mcp_tools( self, ) -> ( - ToolProtocol + Any | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[Any | Callable[..., Any] | MutableMapping[str, Any]] ): pass @@ -144,8 +145,8 @@ async def prepare_agent_infos(self) -> list[AgentInfo]: async def create_agents( self, agent_infos: list[AgentInfo], process_id: str - ) -> list[ChatAgent]: - agents = dict[str, ChatAgent]() + ) -> list[Agent]: + agents = dict[str, Agent]() agent_client = await self.get_client(thread_id=process_id) # Workspace context — injected into every agent's system instructions diff --git a/src/processor/src/libs/mcp_server/MCPBlobIOTool.py b/src/processor/src/libs/mcp_server/MCPBlobIOTool.py index 40a68fe2..f821c925 100644 --- a/src/processor/src/libs/mcp_server/MCPBlobIOTool.py +++ b/src/processor/src/libs/mcp_server/MCPBlobIOTool.py @@ -22,14 +22,14 @@ from libs.mcp_server.MCPBlobIOTool import get_blob_file_mcp from libs.agent_framework.mcp_context import MCPContext - from agent_framework import ChatAgent + from agent_framework import Agent # Get the Blob Storage MCP tool blob_tool = get_blob_file_mcp() # Use with MCPContext for TaskGroup-safe management async with MCPContext(tools=[blob_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run( "Upload the file 'data.csv' to my Azure storage container 'datasets'" ) @@ -76,7 +76,7 @@ def get_blob_file_mcp() -> MCPStdioTool: blob_tool = get_blob_file_mcp() async with blob_tool: - async with ChatAgent(client, tools=[blob_tool]) as agent: + async with Agent(client, tools=[blob_tool]) as agent: result = await agent.run( "Upload 'report.pdf' to container 'documents'" ) @@ -91,7 +91,7 @@ def get_blob_file_mcp() -> MCPStdioTool: blob_tool = get_blob_file_mcp() async with MCPContext(tools=[blob_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: # List all containers containers = await agent.run("List all my blob containers") print(containers) @@ -111,13 +111,13 @@ def get_blob_file_mcp() -> MCPStdioTool: async with MCPContext(tools=[blob_tool, datetime_tool]) as mcp_ctx: # Data processing agent - async with ChatAgent(client1, tools=mcp_ctx.tools) as processor: + async with Agent(client1, tools=mcp_ctx.tools) as processor: data = await processor.run( "Download 'raw_data.csv' from 'input-container'" ) # Analysis agent - async with ChatAgent(client2, tools=mcp_ctx.tools) as analyst: + async with Agent(client2, tools=mcp_ctx.tools) as analyst: result = await analyst.run( f"Analyze the data and upload results to 'output-container'" ) @@ -137,7 +137,7 @@ def get_blob_file_mcp() -> MCPStdioTool: blob_tool = get_blob_file_mcp() async with MCPContext(tools=[blob_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run("Upload 'image.png' to 'media-container'") Note: diff --git a/src/processor/src/libs/mcp_server/MCPDatetimeTool.py b/src/processor/src/libs/mcp_server/MCPDatetimeTool.py index 83aca397..157d07a2 100644 --- a/src/processor/src/libs/mcp_server/MCPDatetimeTool.py +++ b/src/processor/src/libs/mcp_server/MCPDatetimeTool.py @@ -15,14 +15,14 @@ from libs.mcp_server.MCPDatetimeTool import get_datetime_mcp from libs.agent_framework.mcp_context import MCPContext - from agent_framework import ChatAgent + from agent_framework import Agent # Get the datetime MCP tool datetime_tool = get_datetime_mcp() # Use with MCPContext for TaskGroup-safe management async with MCPContext(tools=[datetime_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run("What time is it right now?") print(response) """ @@ -60,7 +60,7 @@ def get_datetime_mcp() -> MCPStdioTool: datetime_tool = get_datetime_mcp() async with datetime_tool: - async with ChatAgent(client, tools=[datetime_tool]) as agent: + async with Agent(client, tools=[datetime_tool]) as agent: result = await agent.run("What's today's date?") print(result) @@ -74,7 +74,7 @@ def get_datetime_mcp() -> MCPStdioTool: weather_tool = get_weather_mcp() async with MCPContext(tools=[datetime_tool, weather_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run( "What's the current time and what's the weather like?" ) @@ -88,10 +88,10 @@ def get_datetime_mcp() -> MCPStdioTool: async with MCPContext(tools=[datetime_tool]) as mcp_ctx: # Share tool across multiple agents - async with ChatAgent(client1, tools=mcp_ctx.tools) as agent1: + async with Agent(client1, tools=mcp_ctx.tools) as agent1: time_info = await agent1.run("Get the current time") - async with ChatAgent(client2, tools=mcp_ctx.tools) as agent2: + async with Agent(client2, tools=mcp_ctx.tools) as agent2: schedule = await agent2.run( f"Based on the time {time_info}, suggest a meeting slot" ) diff --git a/src/processor/src/libs/mcp_server/MCPMicrosoftDocs.py b/src/processor/src/libs/mcp_server/MCPMicrosoftDocs.py index d9a2ca0e..989f7d75 100644 --- a/src/processor/src/libs/mcp_server/MCPMicrosoftDocs.py +++ b/src/processor/src/libs/mcp_server/MCPMicrosoftDocs.py @@ -12,14 +12,14 @@ from libs.mcp_server.MCPMicrosoftDocs import get_microsoft_docs_mcp from libs.agent_framework.mcp_context import MCPContext - from agent_framework import ChatAgent + from agent_framework import Agent # Get the Microsoft Docs MCP tool docs_tool = get_microsoft_docs_mcp() # Use with MCPContext for TaskGroup-safe management async with MCPContext(tools=[docs_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run("Search Microsoft Learn for Azure Functions best practices") print(response) """ @@ -47,7 +47,7 @@ def get_microsoft_docs_mcp() -> MCPStreamableHTTPTool: docs_tool = get_microsoft_docs_mcp() async with docs_tool: - async with ChatAgent(client, tools=[docs_tool]) as agent: + async with Agent(client, tools=[docs_tool]) as agent: result = await agent.run("Find documentation about Azure App Service") Advanced usage with multiple tools: @@ -60,7 +60,7 @@ def get_microsoft_docs_mcp() -> MCPStreamableHTTPTool: datetime_tool = MCPStdioTool(name="datetime", command="npx", args=["-y", "@modelcontextprotocol/server-datetime"]) async with MCPContext(tools=[docs_tool, datetime_tool]) as mcp_ctx: - async with ChatAgent(client, tools=mcp_ctx.tools) as agent: + async with Agent(client, tools=mcp_ctx.tools) as agent: response = await agent.run("What's the latest Azure Functions documentation?") Note: diff --git a/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py b/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py index 93f8f2f0..1221433f 100644 --- a/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py +++ b/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py @@ -12,7 +12,7 @@ from pathlib import Path from typing import Any, Callable, MutableMapping, Sequence -from agent_framework import MCPStdioTool, MCPStreamableHTTPTool, ToolProtocol +from agent_framework import MCPStdioTool, MCPStreamableHTTPTool from libs.agent_framework.agent_info import AgentInfo from libs.agent_framework.groupchat_orchestrator import ( @@ -98,10 +98,10 @@ async def execute( async def prepare_mcp_tools( self, ) -> ( - ToolProtocol + Any | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[Any | Callable[..., Any] | MutableMapping[str, Any]] ): """Create and return the MCP tools used by analysis agents. diff --git a/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py b/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py index f1fe8b4d..3ee0992b 100644 --- a/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py +++ b/src/processor/src/steps/convert/orchestration/yaml_convert_orchestrator.py @@ -16,7 +16,6 @@ from agent_framework import ( MCPStdioTool, MCPStreamableHTTPTool, - ToolProtocol, ) from libs.agent_framework.agent_info import AgentInfo @@ -107,10 +106,10 @@ async def execute( async def prepare_mcp_tools( self, ) -> ( - ToolProtocol + Any | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[Any | Callable[..., Any] | MutableMapping[str, Any]] ): """Create and return the MCP tools used by conversion agents.""" ms_doc_mcp_tool = MCPStreamableHTTPTool( diff --git a/src/processor/src/steps/design/orchestration/design_orchestrator.py b/src/processor/src/steps/design/orchestration/design_orchestrator.py index d2dd47f0..df53ccc2 100644 --- a/src/processor/src/steps/design/orchestration/design_orchestrator.py +++ b/src/processor/src/steps/design/orchestration/design_orchestrator.py @@ -14,7 +14,6 @@ from agent_framework import ( MCPStdioTool, MCPStreamableHTTPTool, - ToolProtocol, ) from libs.agent_framework.agent_info import AgentInfo @@ -98,10 +97,10 @@ async def execute( async def prepare_mcp_tools( self, ) -> ( - ToolProtocol + Any | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[Any | Callable[..., Any] | MutableMapping[str, Any]] ): """Create and return the MCP tools used by design agents.""" # Create MCP tools (not connected yet) diff --git a/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py b/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py index 0aa6c443..ee87719d 100644 --- a/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py +++ b/src/processor/src/steps/documentation/orchestration/documentation_orchestrator.py @@ -18,7 +18,6 @@ from agent_framework import ( MCPStdioTool, MCPStreamableHTTPTool, - ToolProtocol, ) from libs.agent_framework.agent_info import AgentInfo @@ -112,10 +111,10 @@ async def execute( async def prepare_mcp_tools( self, ) -> ( - ToolProtocol + Any | Callable[..., Any] | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | Sequence[Any | Callable[..., Any] | MutableMapping[str, Any]] ): """Create and return the MCP tools used by documentation agents.""" ms_doc_mcp_tool = MCPStreamableHTTPTool( diff --git a/src/processor/src/steps/migration_processor.py b/src/processor/src/steps/migration_processor.py index 73b2954a..dfc7a227 100644 --- a/src/processor/src/steps/migration_processor.py +++ b/src/processor/src/steps/migration_processor.py @@ -33,14 +33,9 @@ from typing import Any from agent_framework import ( - ExecutorCompletedEvent, - ExecutorFailedEvent, - ExecutorInvokedEvent, Workflow, WorkflowBuilder, - WorkflowFailedEvent, - WorkflowOutputEvent, - WorkflowStartedEvent, + WorkflowEvent, ) from openai import AsyncAzureOpenAI @@ -166,30 +161,16 @@ def _init_workflow(self) -> Workflow: Workflow The built workflow ready to execute. """ + analysis = AnalysisExecutor(id="analysis", app_context=self.app_context) + design = DesignExecutor(id="design", app_context=self.app_context) + yaml_convert = YamlConvertExecutor(id="yaml", app_context=self.app_context) + documentation = DocumentationExecutor( + id="documentation", app_context=self.app_context + ) + workflow = ( - WorkflowBuilder() - .register_executor( - lambda: AnalysisExecutor(id="analysis", app_context=self.app_context), - name="analysis", - ) - .register_executor( - lambda: DesignExecutor(id="design", app_context=self.app_context), - name="design", - ) - .register_executor( - lambda: YamlConvertExecutor(id="yaml", app_context=self.app_context), - name="yaml", - ) - .register_executor( - lambda: DocumentationExecutor( - id="documentation", app_context=self.app_context - ), - name="documentation", - ) - .set_start_executor("analysis") - .add_edge("analysis", "design") - .add_edge("design", "yaml") - .add_edge("yaml", "documentation") + WorkflowBuilder(start_executor=analysis) + .add_chain([analysis, design, yaml_convert, documentation]) .build() ) @@ -254,7 +235,7 @@ async def _create_memory_store( async def run(self, input_data: Analysis_TaskParam) -> Any: """Run the migration workflow. - The workflow is executed via ``run_stream`` and handled as a sequence of + The workflow is executed via ``run(stream=True)`` and handled as a sequence of framework events. This method: - Initializes telemetry for the process. @@ -367,8 +348,9 @@ async def _generate_report_summary( "top_remediations": remediation_titles, } - async for event in self.workflow.run_stream(input_data): - if isinstance(event, WorkflowStartedEvent): + async for event in self.workflow.run(input_data, stream=True): + event: WorkflowEvent + if event.type == "started": logger.info("Workflow started (%s)", event.origin.value) report_collector.set_current_step("analysis", step_phase="start") @@ -377,16 +359,16 @@ async def _generate_report_summary( await telemetry.init_process( process_id=input_data.process_id, step="analysis", phase="start" ) - elif isinstance(event, WorkflowOutputEvent): - # WorkflowOutputEvent carries the step output (success or hard-termination). + elif event.type == "output": + # Workflow "output" event carries the step output (success or hard-termination). # Note: a None payload is an error that must be surfaced clearly. if event.data is None: report_collector.set_current_step( - event.source_executor_id or "unknown" + event.executor_id or "unknown" ) # Build a meaningful error message instead of generic "Workflow output is None" - executor_id = event.source_executor_id or "unknown" + executor_id = event.executor_id or "unknown" error_msg = f"Step '{executor_id}' completed without producing output. This may be caused by context length overflow, agent timeout, or an internal orchestration error. Check processor logs for '[AOAI_CTX_TRIM_STREAM]' or exception details." report_collector.record_failure( @@ -407,13 +389,13 @@ async def _generate_report_summary( await telemetry.record_failure_outcome( process_id=input_data.process_id, - failed_step=event.source_executor_id or "unknown", + failed_step=event.executor_id or "unknown", error_message=error_msg, failure_details=failure_details, execution_time_seconds=( time.perf_counter() - - step_start_perf[event.source_executor_id] - if event.source_executor_id in step_start_perf + - step_start_perf[event.executor_id] + if event.executor_id in step_start_perf else None ), ) @@ -423,7 +405,7 @@ async def _generate_report_summary( # Raise a rich exception so the queue worker reports a meaningful reason. raise WorkflowExecutorFailedException({ - "executor_id": event.source_executor_id or "unknown", + "executor_id": event.executor_id or "unknown", "error_type": "WorkflowOutputMissing", "message": error_msg, "traceback": None, @@ -477,15 +459,15 @@ async def _generate_report_summary( } report_collector.set_current_step( - event.source_executor_id or "unknown" + event.executor_id or "unknown" ) report_collector.record_failure( exception=ValueError( getattr(event.data, "reason", None) - or f"Hard terminated in {event.source_executor_id} step" + or f"Hard terminated in {event.executor_id} step" ), custom_message=getattr(event.data, "reason", None) - or f"Hard terminated in {event.source_executor_id} step", + or f"Hard terminated in {event.executor_id} step", ) failure_details: Any = ( @@ -510,14 +492,14 @@ async def _generate_report_summary( await telemetry.record_failure_outcome( process_id=input_data.process_id, - failed_step=event.source_executor_id or "unknown", + failed_step=event.executor_id or "unknown", error_message=getattr(event.data, "reason", None) - or f"Hard terminated in {event.source_executor_id} step", + or f"Hard terminated in {event.executor_id} step", failure_details=failure_details, execution_time_seconds=( time.perf_counter() - - step_start_perf[event.source_executor_id] - if event.source_executor_id in step_start_perf + - step_start_perf[event.executor_id] + if event.executor_id in step_start_perf else None ), ) @@ -533,21 +515,21 @@ async def _generate_report_summary( logger.info("Workflow output (%s): %s", event.origin.value, event.data) await telemetry.record_step_result( process_id=input_data.process_id, - step_name=event.source_executor_id, + step_name=event.executor_id, step_result=event.data, execution_time_seconds=( time.perf_counter() - - step_start_perf[event.source_executor_id] - if event.source_executor_id in step_start_perf + - step_start_perf[event.executor_id] + if event.executor_id in step_start_perf else None ), ) - if event.source_executor_id in step_start_perf: + if event.executor_id in step_start_perf: report_collector.mark_step_completed( - event.source_executor_id, + event.executor_id, execution_time=time.perf_counter() - - step_start_perf[event.source_executor_id], + - step_start_perf[event.executor_id], ) try: @@ -572,10 +554,10 @@ async def _generate_report_summary( ) return event.data - elif isinstance(event, ExecutorFailedEvent): + elif event.type == "executor_failed": pass # will handle in WorkflowFailedEvent - elif isinstance(event, WorkflowFailedEvent): + elif event.type == "failed": logger.error( "Executor failed (%s): %s [%s]: %s (traceback: %s)", event.origin.value, @@ -644,7 +626,7 @@ async def _generate_report_summary( # Raise a rich exception containing the full WorkflowErrorDetails payload. raise WorkflowExecutorFailedException(event.details) - elif isinstance(event, ExecutorInvokedEvent): + elif event.type == "executor_invoked": # The bug. the first executor's event fired after completing execution. if event.executor_id != "analysis": telemetry: TelemetryManager = ( @@ -660,7 +642,7 @@ async def _generate_report_summary( event.executor_id, event.executor_id.capitalize() ) await telemetry.transition_to_phase( - process_id=event.data.process_id, + process_id=getattr(event.data, "process_id", input_data.process_id), step=event.executor_id, phase=f"Initializing {step_display}", ) @@ -675,7 +657,7 @@ async def _generate_report_summary( # near-zero and incorrect. if event.executor_id not in step_start_perf: step_start_perf[event.executor_id] = time.perf_counter() - elif isinstance(event, ExecutorCompletedEvent): + elif event.type == "executor_completed": # print(f"Executor completed ({event.executor_id}): {event.data}") # Log shared memory stats after each step diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py b/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py index 26fcbfe5..572974a8 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py @@ -144,7 +144,7 @@ def test_chaining_returns_self_each_step(self): class TestBuild: def test_build_passes_all_state_to_chat_agent(self): chat_client = MagicMock() - with patch("libs.agent_framework.agent_builder.ChatAgent") as mock_chat: + with patch("libs.agent_framework.agent_builder.Agent") as mock_chat: agent = ( AgentBuilder(chat_client) .with_instructions("inst") @@ -158,21 +158,22 @@ def test_build_passes_all_state_to_chat_agent(self): ) assert agent is mock_chat.return_value kwargs = mock_chat.call_args.kwargs - assert kwargs["chat_client"] is chat_client + assert kwargs["client"] is chat_client assert kwargs["instructions"] == "inst" assert kwargs["id"] == "id1" assert kwargs["name"] == "name1" assert kwargs["description"] == "desc1" - assert kwargs["temperature"] == 0.3 - assert kwargs["max_tokens"] == 100 - assert kwargs["tool_choice"] == "auto" + default_options = kwargs["default_options"] + assert default_options["temperature"] == 0.3 + assert default_options["max_tokens"] == 100 + assert default_options["tool_choice"] == "auto" assert kwargs["extra"] == 42 class TestStaticFactories: def test_create_agent_invokes_chat_agent(self): chat_client = MagicMock() - with patch("libs.agent_framework.agent_builder.ChatAgent") as mock_chat: + with patch("libs.agent_framework.agent_builder.Agent") as mock_chat: agent = AgentBuilder.create_agent( chat_client=chat_client, instructions="i", @@ -181,10 +182,10 @@ def test_create_agent_invokes_chat_agent(self): ) assert agent is mock_chat.return_value kwargs = mock_chat.call_args.kwargs - assert kwargs["chat_client"] is chat_client + assert kwargs["client"] is chat_client assert kwargs["instructions"] == "i" assert kwargs["name"] == "n" - assert kwargs["temperature"] == 0.4 + assert kwargs["default_options"]["temperature"] == 0.4 def test_create_agent_by_agentinfo_uses_helper_and_creates_client(self): # Build a fake AgentInfo with the minimum surface used by the method @@ -206,7 +207,7 @@ def test_create_agent_by_agentinfo_uses_helper_and_creates_client(self): with patch( "libs.agent_framework.agent_builder.get_bearer_token_provider", return_value="token-provider", - ), patch("libs.agent_framework.agent_builder.ChatAgent") as mock_chat: + ), patch("libs.agent_framework.agent_builder.Agent") as mock_chat: agent = AgentBuilder.create_agent_by_agentinfo( service_id="default", agent_info=agent_info, @@ -216,11 +217,11 @@ def test_create_agent_by_agentinfo_uses_helper_and_creates_client(self): helper.settings.get_service_config.assert_called_once_with("default") helper.create_client.assert_called_once() ck = mock_chat.call_args.kwargs - assert ck["chat_client"] == "client-instance" + assert ck["client"] == "client-instance" assert ck["instructions"] == "instr" assert ck["name"] == "A" assert ck["description"] == "D" - assert ck["temperature"] == 0.2 + assert ck["default_options"]["temperature"] == 0.2 def test_create_agent_by_agentinfo_falls_back_to_system_prompt(self): helper = MagicMock() @@ -241,7 +242,7 @@ def test_create_agent_by_agentinfo_falls_back_to_system_prompt(self): with patch( "libs.agent_framework.agent_builder.get_bearer_token_provider", return_value="tp", - ), patch("libs.agent_framework.agent_builder.ChatAgent") as mock_chat: + ), patch("libs.agent_framework.agent_builder.Agent") as mock_chat: AgentBuilder.create_agent_by_agentinfo( service_id="default", agent_info=agent_info ) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py b/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py index 64a8d415..578a79d5 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py @@ -91,9 +91,9 @@ def test_azure_openai_response_with_retry(self): ) assert client is mock_cls.return_value kwargs = mock_cls.call_args.kwargs - assert kwargs["endpoint"] == "https://x" - assert kwargs["deployment_name"] == "gpt-4" - assert kwargs["ad_token_provider"] == "token" + assert kwargs["azure_endpoint"] == "https://x" + assert kwargs["model"] == "gpt-4" + assert kwargs["credential"] == "token" def test_default_token_provider_when_no_credential(self): with patch( @@ -107,53 +107,58 @@ def test_default_token_provider_when_no_credential(self): endpoint="https://x", deployment_name="gpt-4", ) - assert mock_cls.call_args.kwargs["ad_token_provider"] == "default-token" + assert mock_cls.call_args.kwargs["credential"] == "default-token" def test_azure_openai_chat_completion(self): - # Patch the lazily imported module - fake_module = types.ModuleType("agent_framework.azure") - fake_module.AzureOpenAIChatClient = MagicMock(return_value="chat_client") - with patch.dict(sys.modules, {"agent_framework.azure": fake_module}): - client = AgentFrameworkHelper.create_client( - ClientType.AzureOpenAIChatCompletion, - endpoint="https://x", - deployment_name="gpt-4", - ad_token_provider="t", - ) + with patch( + "libs.agent_framework.agent_framework_helper.OpenAIChatCompletionClient", + create=True, + ) as mock_cls: + mock_cls.return_value = "chat_client" + with patch.dict( + "sys.modules", + {"agent_framework.openai": MagicMock(OpenAIChatCompletionClient=mock_cls)}, + ): + client = AgentFrameworkHelper.create_client( + ClientType.AzureOpenAIChatCompletion, + endpoint="https://x", + deployment_name="gpt-4", + ad_token_provider="t", + ) assert client == "chat_client" - def test_azure_openai_assistant(self): - fake_module = types.ModuleType("agent_framework.azure") - fake_module.AzureOpenAIAssistantsClient = MagicMock(return_value="asst_client") - with patch.dict(sys.modules, {"agent_framework.azure": fake_module}): - client = AgentFrameworkHelper.create_client( + def test_azure_openai_assistant_raises_not_implemented(self): + with pytest.raises(NotImplementedError): + AgentFrameworkHelper.create_client( ClientType.AzureOpenAIAssistant, endpoint="https://x", deployment_name="gpt-4", ad_token_provider="t", ) - assert client == "asst_client" def test_azure_openai_response(self): - fake_module = types.ModuleType("agent_framework.azure") - fake_module.AzureOpenAIResponsesClient = MagicMock(return_value="resp_client") - with patch.dict(sys.modules, {"agent_framework.azure": fake_module}): - client = AgentFrameworkHelper.create_client( - ClientType.AzureOpenAIResponse, - endpoint="https://x", - deployment_name="gpt-4", - ad_token_provider="t", - ) + with patch( + "libs.agent_framework.agent_framework_helper.OpenAIChatClient", + create=True, + ) as mock_cls: + mock_cls.return_value = "resp_client" + with patch.dict( + "sys.modules", + {"agent_framework.openai": MagicMock(OpenAIChatClient=mock_cls)}, + ): + client = AgentFrameworkHelper.create_client( + ClientType.AzureOpenAIResponse, + endpoint="https://x", + deployment_name="gpt-4", + ad_token_provider="t", + ) assert client == "resp_client" - def test_azure_openai_agent(self): - fake_module = types.ModuleType("agent_framework.azure") - fake_module.AzureAIAgentClient = MagicMock(return_value="agent_client") - with patch.dict(sys.modules, {"agent_framework.azure": fake_module}): - client = AgentFrameworkHelper.create_client( + def test_azure_openai_agent_raises_not_implemented(self): + with pytest.raises(NotImplementedError): + AgentFrameworkHelper.create_client( ClientType.AzureOpenAIAgent, project_endpoint="https://proj", model_deployment_name="gpt-4", ad_token_provider="t", ) - assert client == "agent_client" diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index 4939049b..600869a0 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -331,29 +331,28 @@ def test_no_prefix(self): class TestAppendTextChunk: def test_no_text_attr(self): orch = _make_orch() - ev = SimpleNamespace(data=SimpleNamespace()) # no `text` attr + ev = SimpleNamespace() # no `text` attr orch._current_agent_response = [] orch._append_text_chunk(ev) # noop assert orch._current_agent_response == [] def test_falsy_text(self): orch = _make_orch() - ev = SimpleNamespace(data=SimpleNamespace(text="")) + ev = SimpleNamespace(text="") orch._current_agent_response = [] orch._append_text_chunk(ev) assert orch._current_agent_response == [] - def test_text_object_with_text_attr(self): + def test_text_string(self): orch = _make_orch() - text_obj = SimpleNamespace(text="hello") - ev = SimpleNamespace(data=SimpleNamespace(text=text_obj)) + ev = SimpleNamespace(text="hello") orch._current_agent_response = [] orch._append_text_chunk(ev) assert orch._current_agent_response == ["hello"] - def test_text_string(self): + def test_text_raw_string(self): orch = _make_orch() - ev = SimpleNamespace(data=SimpleNamespace(text="raw")) + ev = SimpleNamespace(text="raw") orch._current_agent_response = [] orch._append_text_chunk(ev) assert orch._current_agent_response == ["raw"] @@ -417,20 +416,20 @@ async def _bad_stream(_): class TestProcessToolCalls: def test_no_tool_calls_returns_immediately(self): orch = _make_orch() - ev = SimpleNamespace(data=SimpleNamespace(contents=None)) + ev = SimpleNamespace(contents=None) _run(orch._process_tool_calls(ev, "A", None)) def test_records_complete_dict_args(self): orch = _make_orch() item = SimpleNamespace(name="search", call_id="c1", arguments={"q": "x"}) - ev = SimpleNamespace(data=SimpleNamespace(contents=[item])) + ev = SimpleNamespace(contents=[item]) _run(orch._process_tool_calls(ev, "A", None)) assert "search" in {tc["tool_name"] for tc in orch.agent_tool_usage["A"]} def test_skips_when_already_recorded(self): orch = _make_orch() item = SimpleNamespace(name="search", call_id="c1", arguments={"q": "x"}) - ev = SimpleNamespace(data=SimpleNamespace(contents=[item])) + ev = SimpleNamespace(contents=[item]) _run(orch._process_tool_calls(ev, "A", None)) # second pass should be skipped _run(orch._process_tool_calls(ev, "A", None)) @@ -439,7 +438,7 @@ def test_skips_when_already_recorded(self): def test_skips_invalid_calls(self): orch = _make_orch() item = SimpleNamespace(name=None, call_id=None, arguments=None) - ev = SimpleNamespace(data=SimpleNamespace(contents=[item])) + ev = SimpleNamespace(contents=[item]) _run(orch._process_tool_calls(ev, "A", None)) assert orch.agent_tool_usage == {} @@ -448,13 +447,13 @@ def test_streamed_string_args_buffer_until_complete(self): # Send incomplete JSON args, then complete item1 = SimpleNamespace(name="t", call_id="c", arguments='{"q":"hel') - ev1 = SimpleNamespace(data=SimpleNamespace(contents=[item1])) + ev1 = SimpleNamespace(contents=[item1]) _run(orch._process_tool_calls(ev1, "A", None)) # not yet recorded assert "A" not in orch.agent_tool_usage or not orch.agent_tool_usage["A"] item2 = SimpleNamespace(name="t", call_id="c", arguments='{"q":"hello"}') - ev2 = SimpleNamespace(data=SimpleNamespace(contents=[item2])) + ev2 = SimpleNamespace(contents=[item2]) _run(orch._process_tool_calls(ev2, "A", None)) assert orch.agent_tool_usage["A"][0]["arguments"] == {"q": "hello"} @@ -601,30 +600,27 @@ def test_skips_unrelated(self): class TestBackfillToolUsage: def test_skips_non_assistant(self): - from agent_framework import Role orch = _make_orch() - msg = SimpleNamespace(role=Role.USER, contents=[]) + msg = SimpleNamespace(role="user", contents=[]) orch._backfill_tool_usage_from_conversation([msg]) assert orch.agent_tool_usage == {} def test_records_calls_from_assistant(self): - from agent_framework import Role orch = _make_orch() item = SimpleNamespace(name="t", call_id="c", arguments={"x": 1}) msg = SimpleNamespace( - role=Role.ASSISTANT, author_name="A", contents=[item] + role="assistant", author_name="A", contents=[item] ) orch._backfill_tool_usage_from_conversation([msg]) assert orch.agent_tool_usage["A"][0]["tool_name"] == "t" def test_dedup_already_recorded(self): - from agent_framework import Role orch = _make_orch() # Pre-mark this call as already recorded orch._tool_call_recorded.add(("A", "c")) item = SimpleNamespace(name="t", call_id="c", arguments={}) msg = SimpleNamespace( - role=Role.ASSISTANT, author_name="A", contents=[item] + role="assistant", author_name="A", contents=[item] ) orch._backfill_tool_usage_from_conversation([msg]) assert "A" in orch.agent_tool_usage @@ -719,16 +715,17 @@ def test_build_groupchat_invokes_builder(self): }) with patch("libs.agent_framework.groupchat_orchestrator.GroupChatBuilder") as MockBuilder: built = MagicMock() - built.set_manager.return_value = built - built.participants.return_value = built built.build.return_value = "wf" MockBuilder.return_value = built wf = _run(orch._build_groupchat()) assert wf == "wf" + # Check constructor args + ctor_kwargs = MockBuilder.call_args.kwargs + assert ctor_kwargs["orchestrator_agent"] == "coord" # ResultGenerator excluded from participants - kwargs = built.participants.call_args.args[0] - assert "arch" in kwargs - assert "rg" not in kwargs + participants = ctor_kwargs["participants"] + assert "arch" in participants + assert "rg" not in participants # ----------------------------------------------------------------------------- @@ -776,13 +773,12 @@ def test_tail_zero_returns_head(self): class TestBuildResultGeneratorConversation: def test_excludes_named_authors(self): - from agent_framework import Role - from agent_framework import ChatMessage + from agent_framework import Content, Message orch = _make_orch() msgs = [ - ChatMessage(role=Role.ASSISTANT, text="from coord", author_name="Coordinator"), - ChatMessage(role=Role.ASSISTANT, text="from architect", author_name="Architect"), + Message(role="assistant", contents=[Content.from_text("from coord")], author_name="Coordinator"), + Message(role="assistant", contents=[Content.from_text("from architect")], author_name="Architect"), ] out = orch._build_result_generator_conversation( msgs, @@ -797,14 +793,13 @@ def test_excludes_named_authors(self): assert all("Coordinator" != m.author_name for m in out) def test_dedupes_identical_payloads(self): - from agent_framework import Role - from agent_framework import ChatMessage + from agent_framework import Content, Message orch = _make_orch() big = "X" * 1000 msgs = [ - ChatMessage(role=Role.ASSISTANT, text=big, author_name="A"), - ChatMessage(role=Role.ASSISTANT, text=big, author_name="A"), + Message(role="assistant", contents=[Content.from_text(big)], author_name="A"), + Message(role="assistant", contents=[Content.from_text(big)], author_name="A"), ] out = orch._build_result_generator_conversation( msgs, @@ -818,12 +813,11 @@ def test_dedupes_identical_payloads(self): assert len(out) == 1 def test_truncates_messages_to_per_message_budget(self): - from agent_framework import Role - from agent_framework import ChatMessage + from agent_framework import Content, Message orch = _make_orch() msgs = [ - ChatMessage(role=Role.ASSISTANT, text="A" * 500, author_name="X"), + Message(role="assistant", contents=[Content.from_text("A" * 500)], author_name="X"), ] out = orch._build_result_generator_conversation( msgs, @@ -837,12 +831,11 @@ def test_truncates_messages_to_per_message_budget(self): assert len(out[-1].text) <= 100 def test_total_budget_enforced(self): - from agent_framework import Role - from agent_framework import ChatMessage + from agent_framework import Content, Message orch = _make_orch() msgs = [ - ChatMessage(role=Role.ASSISTANT, text="A" * 100, author_name=str(i)) + Message(role="assistant", contents=[Content.from_text("A" * 100)], author_name=str(i)) for i in range(20) ] out = orch._build_result_generator_conversation( @@ -858,12 +851,11 @@ def test_total_budget_enforced(self): assert total <= 200 def test_max_messages_caps_count(self): - from agent_framework import Role - from agent_framework import ChatMessage + from agent_framework import Content, Message orch = _make_orch() msgs = [ - ChatMessage(role=Role.ASSISTANT, text=f"m{i}", author_name=str(i)) + Message(role="assistant", contents=[Content.from_text(f"m{i}")], author_name=str(i)) for i in range(20) ] out = orch._build_result_generator_conversation( @@ -915,8 +907,7 @@ def test_unknown_tool_name(self): class TestGenerateFinalResult: def test_parses_valid_json(self): from pydantic import BaseModel - from agent_framework import Role - from agent_framework import ChatMessage + from agent_framework import Content, Message class Model(BaseModel): x: int @@ -927,7 +918,7 @@ class Model(BaseModel): orch = _make_orch(participants={"Coordinator": object(), "ResultGenerator": rg}, result_format=Model) out = _run( orch._generate_final_result( - conversation=[ChatMessage(role=Role.ASSISTANT, text="x", author_name="A")], + conversation=[Message(role="assistant", contents=[Content.from_text("x")], author_name="A")], result_format=Model, result_generator_name="ResultGenerator", ) @@ -936,8 +927,7 @@ class Model(BaseModel): def test_retry_on_validation_error(self): from pydantic import BaseModel - from agent_framework import Role - from agent_framework import ChatMessage + from agent_framework import Content, Message class Model(BaseModel): x: int @@ -950,7 +940,7 @@ class Model(BaseModel): orch = _make_orch(participants={"Coordinator": object(), "ResultGenerator": rg}, result_format=Model) out = _run( orch._generate_final_result( - conversation=[ChatMessage(role=Role.ASSISTANT, text="x", author_name="A")], + conversation=[Message(role="assistant", contents=[Content.from_text("x")], author_name="A")], result_format=Model, result_generator_name="ResultGenerator", ) @@ -968,8 +958,9 @@ class TestHandleAgentUpdate: def test_invokes_subroutines(self): orch = _make_orch() ev = SimpleNamespace( - executor_id="groupchat_agent:A", - data=SimpleNamespace(text="chunk", contents=None), + agent_id="groupchat_agent:A", + text="chunk", + contents=None, ) _run(orch._handle_agent_update(ev, None, None)) assert orch._last_executor_id == "A" diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py index 7556b989..d9fc0851 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_input_observer_middleware.py @@ -4,7 +4,7 @@ import asyncio from types import SimpleNamespace -from agent_framework import ChatMessage, Role +from agent_framework import Content, Message from libs.agent_framework.middlewares import InputObserverMiddleware @@ -13,7 +13,7 @@ def test_input_observer_middleware_replaces_user_text_when_configured() -> None: async def _run() -> None: ctx = SimpleNamespace( messages=[ - ChatMessage(role=Role.USER, text="original"), + Message(role="user", contents=[Content.from_text("original")]), ] ) @@ -24,7 +24,7 @@ async def _next(_context): await mw.process(ctx, _next) - assert ctx.messages[0].role == Role.USER + assert ctx.messages[0].role == "user" assert ctx.messages[0].text == "replacement" asyncio.run(_run()) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py index c4c32f5a..f9ed0f4e 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_middlewares_extras.py @@ -7,7 +7,7 @@ from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock -from agent_framework import ChatMessage, Role +from agent_framework import Content, Message from libs.agent_framework.middlewares import ( DebuggingMiddleware, @@ -86,8 +86,8 @@ class TestInputObserverMiddleware: def test_replaces_user_messages_when_replacement_set(self): from libs.agent_framework.middlewares import InputObserverMiddleware - msg_user = ChatMessage(role=Role.USER, text="orig user") - msg_assistant = ChatMessage(role=Role.ASSISTANT, text="hi") + msg_user = Message(role="user", contents=[Content.from_text("orig user")]) + msg_assistant = Message(role="assistant", contents=[Content.from_text("hi")]) ctx = MagicMock() ctx.messages = [msg_user, msg_assistant] next_fn = AsyncMock() @@ -101,7 +101,7 @@ def test_replaces_user_messages_when_replacement_set(self): def test_no_replacement_keeps_text(self): from libs.agent_framework.middlewares import InputObserverMiddleware - msg = ChatMessage(role=Role.USER, text="keep me") + msg = Message(role="user", contents=[Content.from_text("keep me")]) ctx = MagicMock() ctx.messages = [msg] mw = InputObserverMiddleware(replacement=None) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py b/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py index 1d75ee7a..2f9e1948 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_shared_memory_context_provider.py @@ -6,6 +6,7 @@ from __future__ import annotations import asyncio +from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock from libs.agent_framework.qdrant_memory_store import MemoryEntry @@ -20,8 +21,7 @@ def _make_chat_message(text: str, role: str = "assistant") -> MagicMock: msg = MagicMock() msg.text = text msg.content = text - msg.role = MagicMock() - msg.role.value = role + msg.role = role return msg @@ -61,8 +61,18 @@ def _make_provider(store=None): ), store +def _make_session_context(input_messages=None, response=None): + """Create a fake SessionContext for testing.""" + ctx = SimpleNamespace( + input_messages=input_messages or [], + instructions=None, + response=response, + ) + return ctx + + # --------------------------------------------------------------------------- -# invoking() — Pre-LLM memory injection +# before_run() - Pre-LLM memory injection # --------------------------------------------------------------------------- @@ -74,12 +84,13 @@ async def _run(): _make_memory_entry("Azure Files for AKS", agent_name="AKS Expert"), ] messages = [_make_chat_message("How should we handle storage configuration?")] + ctx = _make_session_context(input_messages=messages) - context = await provider.invoking(messages) + await provider.before_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) - assert context.instructions is not None - assert "GKE Filestore CSI" in context.instructions - assert "Azure Files for AKS" in context.instructions + assert ctx.instructions is not None + assert "GKE Filestore CSI" in ctx.instructions[0] + assert "Azure Files for AKS" in ctx.instructions[0] store.search.assert_called_once() asyncio.run(_run()) @@ -88,9 +99,10 @@ async def _run(): def test_invoking_empty_messages_returns_empty(): async def _run(): provider, _ = _make_provider() - context = await provider.invoking([]) - assert context.instructions is None - assert context.messages == [] + ctx = _make_session_context(input_messages=[]) + + await provider.before_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) + assert ctx.instructions is None asyncio.run(_run()) @@ -100,9 +112,10 @@ async def _run(): provider, store = _make_provider() store.search.return_value = [] messages = [_make_chat_message("What is the overall migration plan for AKS?")] + ctx = _make_session_context(input_messages=messages) - context = await provider.invoking(messages) - assert context.instructions is None + await provider.before_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) + assert ctx.instructions is None asyncio.run(_run()) @@ -112,9 +125,10 @@ async def _run(): provider, store = _make_provider() store.search.side_effect = Exception("search failed") messages = [_make_chat_message("What is the networking plan for AKS?")] + ctx = _make_session_context(input_messages=messages) - context = await provider.invoking(messages) - assert context.instructions is None + await provider.before_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) + assert ctx.instructions is None asyncio.run(_run()) @@ -124,8 +138,9 @@ async def _run(): provider, store = _make_provider() long_text = "x" * 5000 messages = [_make_chat_message(long_text)] + ctx = _make_session_context(input_messages=messages) - await provider.invoking(messages) + await provider.before_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) query = store.search.call_args.kwargs["query"] assert len(query) <= 2000 @@ -141,8 +156,9 @@ async def _run(): _make_chat_message("Second"), _make_chat_message("Latest question about storage"), ] + ctx = _make_session_context(input_messages=messages) - await provider.invoking(messages) + await provider.before_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) query = store.search.call_args.kwargs["query"] assert "Latest question about storage" in query @@ -158,11 +174,12 @@ async def _run(): ] store.search.return_value = large_memories messages = [_make_chat_message("What storage configuration should we use for persistent volumes?")] + ctx = _make_session_context(input_messages=messages) - context = await provider.invoking(messages) + await provider.before_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) - assert context.instructions is not None - assert len(context.instructions) <= MAX_MEMORY_CONTEXT_CHARS + 200 + assert ctx.instructions is not None + assert len(ctx.instructions[0]) <= MAX_MEMORY_CONTEXT_CHARS + 200 asyncio.run(_run()) @@ -174,11 +191,12 @@ async def _run(): _make_memory_entry("Use Premium SSD", agent_name="Chief Architect", step="design"), ] messages = [_make_chat_message("What storage class should we choose for the cluster?")] + ctx = _make_session_context(input_messages=messages) - context = await provider.invoking(messages) + await provider.before_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) - assert "Chief Architect" in context.instructions - assert "design" in context.instructions + assert "Chief Architect" in ctx.instructions[0] + assert "design" in ctx.instructions[0] asyncio.run(_run()) @@ -188,27 +206,35 @@ async def _run(): provider, store = _make_provider() store.search.return_value = [_make_memory_entry("some memory")] single = _make_chat_message("What about networking configuration for AKS?") + ctx = _make_session_context(input_messages=[single]) - context = await provider.invoking(single) + await provider.before_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) - assert context.instructions is not None + assert ctx.instructions is not None store.search.assert_called_once() asyncio.run(_run()) # --------------------------------------------------------------------------- -# invoked() — Post-LLM memory storage +# after_run() - Post-LLM memory storage # --------------------------------------------------------------------------- +def _make_response_with_messages(messages): + """Create a mock response object with messages attribute.""" + resp = SimpleNamespace(messages=messages) + return resp + + def test_invoked_stores_response(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("What is the networking plan for AKS?")] - response = [_make_chat_message("We should use Azure CNI for networking configuration in the AKS cluster")] + response_msgs = [_make_chat_message("We should use Azure CNI for networking configuration in the AKS cluster")] + response = _make_response_with_messages(response_msgs) + ctx = _make_session_context(response=response) - await provider.invoked(request, response) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) await provider.flush() store.add.assert_called_once() @@ -222,10 +248,10 @@ async def _run(): def test_invoked_skips_on_exception(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("Q")] - response = [_make_chat_message("A" * 100)] + # No response (simulating an exception scenario) + ctx = _make_session_context(response=None) - await provider.invoked(request, response, invoke_exception=Exception("fail")) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) store.add.assert_not_called() asyncio.run(_run()) @@ -234,9 +260,9 @@ async def _run(): def test_invoked_skips_none_response(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("Q")] + ctx = _make_session_context(response=None) - await provider.invoked(request, None) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) store.add.assert_not_called() asyncio.run(_run()) @@ -245,10 +271,11 @@ async def _run(): def test_invoked_skips_short_response(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("Q")] - short = [_make_chat_message("x" * (MIN_CONTENT_LENGTH_TO_STORE - 1))] + short_msgs = [_make_chat_message("x" * (MIN_CONTENT_LENGTH_TO_STORE - 1))] + response = _make_response_with_messages(short_msgs) + ctx = _make_session_context(response=response) - await provider.invoked(request, short) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) store.add.assert_not_called() asyncio.run(_run()) @@ -257,10 +284,11 @@ async def _run(): def test_invoked_stores_long_response(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("Q")] - long_resp = [_make_chat_message("x" * (MIN_CONTENT_LENGTH_TO_STORE + 1))] + long_msgs = [_make_chat_message("x" * (MIN_CONTENT_LENGTH_TO_STORE + 1))] + response = _make_response_with_messages(long_msgs) + ctx = _make_session_context(response=response) - await provider.invoked(request, long_resp) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) await provider.flush() store.add.assert_called_once() @@ -270,11 +298,12 @@ async def _run(): def test_invoked_increments_turn_counter(): async def _run(): provider, store = _make_provider() - request = [_make_chat_message("Q")] - response = [_make_chat_message("A" * 100)] + response_msgs = [_make_chat_message("A" * 100)] + response = _make_response_with_messages(response_msgs) + ctx = _make_session_context(response=response) - await provider.invoked(request, response) - await provider.invoked(request, response) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) assert provider._turn_counter == 2 asyncio.run(_run()) @@ -284,10 +313,11 @@ def test_invoked_store_failure_does_not_raise(): async def _run(): provider, store = _make_provider() store.add.side_effect = Exception("store failed") - request = [_make_chat_message("Q")] - response = [_make_chat_message("A" * 100)] + response_msgs = [_make_chat_message("A" * 100)] + response = _make_response_with_messages(response_msgs) + ctx = _make_session_context(response=response) - await provider.invoked(request, response) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) await provider.flush() # Should not raise asyncio.run(_run()) @@ -296,10 +326,11 @@ async def _run(): def test_invoked_with_single_message(): async def _run(): provider, store = _make_provider() - request = _make_chat_message("What is the question about networking?") - response = _make_chat_message("We should use Azure CNI Overlay for the networking configuration in AKS") + response_msgs = [_make_chat_message("We should use Azure CNI Overlay for the networking configuration in AKS")] + response = _make_response_with_messages(response_msgs) + ctx = _make_session_context(response=response) - await provider.invoked(request, response) + await provider.after_run(agent=MagicMock(), session=MagicMock(), context=ctx, state={}) await provider.flush() store.add.assert_called_once() diff --git a/src/processor/src/tests/unit/steps/test_migration_processor_run.py b/src/processor/src/tests/unit/steps/test_migration_processor_run.py index acd4ee40..1f05bb85 100644 --- a/src/processor/src/tests/unit/steps/test_migration_processor_run.py +++ b/src/processor/src/tests/unit/steps/test_migration_processor_run.py @@ -12,12 +12,7 @@ import pytest from agent_framework import ( - ExecutorCompletedEvent, - ExecutorFailedEvent, - ExecutorInvokedEvent, - WorkflowFailedEvent, - WorkflowOutputEvent, - WorkflowStartedEvent, + WorkflowEvent, ) from agent_framework._workflows._events import WorkflowErrorDetails @@ -61,12 +56,12 @@ def _make_processor(events: list, memory_store=None) -> MigrationProcessor: proc._telemetry = telemetry # expose for assertions - async def _stream(_input): + async def _stream(_input, **kwargs): for ev in events: yield ev workflow = MagicMock() - workflow.run_stream = _stream + workflow.run = _stream proc.workflow = workflow # Patch _create_memory_store as an AsyncMock returning the provided value. @@ -79,11 +74,11 @@ class TestRunSuccessFlow: def test_workflow_started_then_normal_output_returns_data(self): data = SimpleNamespace(is_hard_terminated=False, value="ok") events = [ - WorkflowStartedEvent(), - ExecutorInvokedEvent(executor_id="analysis", data=_make_input()), - ExecutorCompletedEvent(executor_id="analysis", data={"r": 1}), - ExecutorInvokedEvent(executor_id="design", data=_make_input()), - WorkflowOutputEvent(data=data, source_executor_id="design"), + WorkflowEvent.started(), + WorkflowEvent.executor_invoked("analysis", _make_input()), + WorkflowEvent.executor_completed("analysis", {"r": 1}), + WorkflowEvent.executor_invoked("design", _make_input()), + WorkflowEvent.output("design", data), ] proc = _make_processor(events) result = _run(proc.run(_make_input())) @@ -96,10 +91,10 @@ def test_workflow_started_then_normal_output_returns_data(self): def test_invoked_event_for_non_analysis_triggers_transition_phase(self): data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), + WorkflowEvent.started(), # Documentation invocation should map to "Documentation" display - ExecutorInvokedEvent(executor_id="documentation", data=_make_input()), - WorkflowOutputEvent(data=data, source_executor_id="documentation"), + WorkflowEvent.executor_invoked("documentation", _make_input()), + WorkflowEvent.output("documentation", data), ] proc = _make_processor(events) _run(proc.run(_make_input())) @@ -112,9 +107,9 @@ def test_invoked_event_for_non_analysis_triggers_transition_phase(self): def test_invoked_event_unknown_executor_uses_capitalize(self): data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), - ExecutorInvokedEvent(executor_id="custom", data=_make_input()), - WorkflowOutputEvent(data=data, source_executor_id="custom"), + WorkflowEvent.started(), + WorkflowEvent.executor_invoked("custom", _make_input()), + WorkflowEvent.output("custom", data), ] proc = _make_processor(events) _run(proc.run(_make_input())) @@ -132,8 +127,8 @@ def test_hard_terminated_returns_data_and_records_failure(self): blocking_issues=["NEED_HUMAN_REVIEW"], ) events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.output("analysis", data), ] proc = _make_processor(events) result = _run(proc.run(_make_input())) @@ -150,8 +145,8 @@ def test_hard_terminated_security_policy_collects_evidence(self): blocking_issues=["SECURITY_POLICY_VIOLATION"], ) events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.output("analysis", data), ] proc = _make_processor(events) @@ -181,8 +176,8 @@ def test_hard_terminated_security_policy_handles_collector_error(self): blocking_issues=["SECURITY_POLICY_VIOLATION"], ) events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.output("analysis", data), ] proc = _make_processor(events) with patch( @@ -198,8 +193,8 @@ def test_hard_terminated_security_policy_handles_collector_error(self): class TestRunOutputMissingFlow: def test_missing_output_raises_workflow_executor_failed_exception(self): events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=None, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.output("analysis", None), ] proc = _make_processor(events) with pytest.raises(WorkflowExecutorFailedException) as excinfo: @@ -209,8 +204,8 @@ def test_missing_output_raises_workflow_executor_failed_exception(self): def test_missing_output_with_none_source_uses_unknown(self): events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=None, source_executor_id=None), + WorkflowEvent.started(), + WorkflowEvent.output(None, None), ] proc = _make_processor(events) with pytest.raises(WorkflowExecutorFailedException): @@ -226,9 +221,9 @@ def test_workflow_failed_event_raises_with_details(self): executor_id="yaml", ) events = [ - WorkflowStartedEvent(), - ExecutorInvokedEvent(executor_id="yaml", data=_make_input()), - WorkflowFailedEvent(details=details), + WorkflowEvent.started(), + WorkflowEvent.executor_invoked("yaml", _make_input()), + WorkflowEvent.failed(details), ] proc = _make_processor(events) with pytest.raises(WorkflowExecutorFailedException) as excinfo: @@ -246,8 +241,8 @@ def test_workflow_failed_classifies_context_size_message(self): executor_id="design", ) events = [ - WorkflowStartedEvent(), - WorkflowFailedEvent(details=details), + WorkflowEvent.started(), + WorkflowEvent.failed(details), ] proc = _make_processor(events) with pytest.raises(WorkflowExecutorFailedException): @@ -261,23 +256,23 @@ def test_workflow_failed_classifies_context_error_type(self): executor_id="analysis", ) events = [ - WorkflowStartedEvent(), - WorkflowFailedEvent(details=details), + WorkflowEvent.started(), + WorkflowEvent.failed(details), ] proc = _make_processor(events) with pytest.raises(WorkflowExecutorFailedException): _run(proc.run(_make_input())) def test_executor_failed_event_is_silently_ignored(self): - # ExecutorFailedEvent does not raise on its own; WorkflowFailedEvent does. + # executor_failed event does not raise on its own; workflow "failed" event does. details = WorkflowErrorDetails( error_type="X", message="m", traceback=None, executor_id="analysis" ) data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), - ExecutorFailedEvent(executor_id="analysis", details=details), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.executor_failed("analysis", details), + WorkflowEvent.output("analysis", data), ] proc = _make_processor(events) result = _run(proc.run(_make_input())) @@ -288,9 +283,9 @@ class TestRunMemoryStoreLifecycle: def test_memory_store_is_registered_and_closed(self): data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), - ExecutorCompletedEvent(executor_id="analysis", data=None), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.executor_completed("analysis", None), + WorkflowEvent.output("analysis", data), ] memory_store = MagicMock() memory_store.get_count = AsyncMock(return_value=3) @@ -304,8 +299,8 @@ def test_memory_store_is_registered_and_closed(self): def test_memory_store_close_error_is_swallowed(self): data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), - WorkflowOutputEvent(data=data, source_executor_id="analysis"), + WorkflowEvent.started(), + WorkflowEvent.output("analysis", data), ] memory_store = MagicMock() memory_store.get_count = AsyncMock(side_effect=RuntimeError("x")) @@ -318,18 +313,18 @@ def test_memory_store_close_error_is_swallowed(self): def test_executor_completed_with_memory_store_logs_count(self): data = SimpleNamespace(is_hard_terminated=False) events = [ - WorkflowStartedEvent(), - ExecutorCompletedEvent( - executor_id="analysis", data={"some": "result"} + WorkflowEvent.started(), + WorkflowEvent.executor_completed( + "analysis", {"some": "result"} ), - WorkflowOutputEvent(data=data, source_executor_id="design"), + WorkflowEvent.output("design", data), ] memory_store = MagicMock() memory_store.get_count = AsyncMock(return_value=7) memory_store.close = AsyncMock() proc = _make_processor(events, memory_store=memory_store) _run(proc.run(_make_input())) - # get_count called at least once during ExecutorCompletedEvent and at finally + # get_count called at least once during executor_completed event and at finally assert memory_store.get_count.await_count >= 2 # record_step_result called for the executor completed event with data proc._telemetry.record_step_result.assert_any_await( diff --git a/src/processor/uv.lock b/src/processor/uv.lock index 0f3c189b..10c1b264 100644 --- a/src/processor/uv.lock +++ b/src/processor/uv.lock @@ -51,14 +51,14 @@ wheels = [ [[package]] name = "agent-framework" -version = "1.0.0b260107" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "agent-framework-core", extra = ["all"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/e7/5ad52075da4e586ca94fb8806b3085ac5dea8059413e413bff88c0452e88/agent_framework-1.0.0b260107.tar.gz", hash = "sha256:a2f6508a0ca1df3b7ca4e3a64e45bac8e33cdfe02cf69e9056e37e881a58aad7", size = 2898189, upload-time = "2026-01-07T23:57:48.213Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/e8/c2ee1c4dae4a86b95091969426d11361232a0c554124ba321852a6b6b9bd/agent_framework-1.3.0.tar.gz", hash = "sha256:a13423aceaf587cf28180138151d445bd2d4ce82908cef4a6fbb85fa1771bac1", size = 5509571, upload-time = "2026-05-08T00:09:16.022Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/55/ffef27526cc26bf163ccf9d58ba87bf4e677bba343a542e7b666846f744d/agent_framework-1.0.0b260107-py3-none-any.whl", hash = "sha256:080deb32bff4ef07227a4ba709798c67079ff8a2997fe7a0aed0010adc0c18cf", size = 5554, upload-time = "2026-01-07T23:57:08.433Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/050f8f8bce8c629a88197837b4beb35cb287f880789fc01923fd5938f142/agent_framework-1.3.0-py3-none-any.whl", hash = "sha256:baaaa932639c87be99d43333f612c3b4112d6d976f0e1e72238e42a4bd572438", size = 5684, upload-time = "2026-05-08T00:09:54.064Z" }, ] [[package]] @@ -103,31 +103,29 @@ wheels = [ ] [[package]] -name = "agent-framework-azure-ai" +name = "agent-framework-azure-ai-search" version = "1.0.0b260130" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "agent-framework-core" }, - { name = "aiohttp" }, - { name = "azure-ai-agents" }, - { name = "azure-ai-projects" }, + { name = "azure-search-documents" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/ef/69ead4fcd2c21608ce35353a507df23df51872552747f803c43d1d81f612/agent_framework_azure_ai-1.0.0b260130.tar.gz", hash = "sha256:c571275089a801f961370ba824568c8b02143b1a6bb5b1d78b97c6debdf4906f", size = 32723, upload-time = "2026-01-30T18:56:41.07Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/63/81c7853aa526f3c3667871cea14667af73323c6c53d31c34be34926a9de4/agent_framework_azure_ai_search-1.0.0b260130.tar.gz", hash = "sha256:0a622fdddd7dc0287de693f2aa6f770ec52ea8d1eaca817c4276daa08001c10b", size = 13312, upload-time = "2026-01-30T19:01:08.046Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/8f/a1467c352fed5eb6ebb9567109251cc39b5b3ebb5137a2d14c71fea51bc8/agent_framework_azure_ai-1.0.0b260130-py3-none-any.whl", hash = "sha256:87f0248fe6d4f2f4146f0a56a53527af6365d4a377dc2e3d56c37cbb9deae098", size = 38542, upload-time = "2026-01-30T19:01:12.102Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ec/ac8143dbb1af2ec510f7772d712803193a6a0ad5f36b06e7ec7121df5c80/agent_framework_azure_ai_search-1.0.0b260130-py3-none-any.whl", hash = "sha256:0278c948696d7a00193a0271074c6057b57589ff98eda5544f2eafeac051d6e9", size = 13449, upload-time = "2026-01-30T19:01:23.262Z" }, ] [[package]] -name = "agent-framework-azure-ai-search" -version = "1.0.0b260130" +name = "agent-framework-azure-cosmos" +version = "1.0.0b260507" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "agent-framework-core" }, - { name = "azure-search-documents" }, + { name = "azure-cosmos" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/63/81c7853aa526f3c3667871cea14667af73323c6c53d31c34be34926a9de4/agent_framework_azure_ai_search-1.0.0b260130.tar.gz", hash = "sha256:0a622fdddd7dc0287de693f2aa6f770ec52ea8d1eaca817c4276daa08001c10b", size = 13312, upload-time = "2026-01-30T19:01:08.046Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/97/fd8b045fc4eb1d213d7a91eff6e48e030fdb67da30505f46f1ed20a7aa48/agent_framework_azure_cosmos-1.0.0b260507.tar.gz", hash = "sha256:2c8ec2d5eae52b9e92fd14b4adecd5a52a900a7897589549c32852d9488112c7", size = 10984, upload-time = "2026-05-08T00:09:22.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/ec/ac8143dbb1af2ec510f7772d712803193a6a0ad5f36b06e7ec7121df5c80/agent_framework_azure_ai_search-1.0.0b260130-py3-none-any.whl", hash = "sha256:0278c948696d7a00193a0271074c6057b57589ff98eda5544f2eafeac051d6e9", size = 13449, upload-time = "2026-01-30T19:01:23.262Z" }, + { url = "https://files.pythonhosted.org/packages/84/b9/6ac1960dae49ecde8ea906b302abe79b66d09d4cf74f8ed3f7dd9fc6230f/agent_framework_azure_cosmos-1.0.0b260507-py3-none-any.whl", hash = "sha256:c1d7ae4a560b592d2bff9c1ec75a7910101baf8c1778443644cc8cb81c82c1a1", size = 11989, upload-time = "2026-05-08T00:09:02.858Z" }, ] [[package]] @@ -146,6 +144,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/fa/200b40db670f79f561ff1e69e9626729ceb6486af970e3489f6c3a295d76/agent_framework_azurefunctions-1.0.0b260130-py3-none-any.whl", hash = "sha256:7d529a0bad67caa38d8823462c439e97de5e1cf364c0e9a0895df5fb44996f64", size = 17788, upload-time = "2026-01-30T18:56:45.741Z" }, ] +[[package]] +name = "agent-framework-bedrock" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "boto3" }, + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/86/0b7dd9d1c043b251ff8bd0e037a20495c82c798914db0372040625cae889/agent_framework_bedrock-1.0.0b260507.tar.gz", hash = "sha256:38953ab30f7aff651a9c85c1ceeefd2ad85fa094b3316858930f1c18dcaff2c6", size = 17467, upload-time = "2026-05-08T00:09:24.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/b4/fc4277a50b7a0a7cd038e4511a0215fb98ab5e394f719506e30c31854335/agent_framework_bedrock-1.0.0b260507-py3-none-any.whl", hash = "sha256:28ce485c639e467ca4fae4d5b747cd7f9438b8145ca096c658ab5c694611edcc", size = 13907, upload-time = "2026-05-08T00:09:18.84Z" }, +] + [[package]] name = "agent-framework-chatkit" version = "1.0.0b260130" @@ -159,6 +171,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/f1/68496e52aa36e66cf2962b8a8c6937053e2e57ad5f135b6983d705172554/agent_framework_chatkit-1.0.0b260130-py3-none-any.whl", hash = "sha256:a7814a5b222de7a0ac57fb89f4a6e534521c7e58bdc86a6465885fb9d57e63f1", size = 11712, upload-time = "2026-01-30T18:56:49.14Z" }, ] +[[package]] +name = "agent-framework-claude" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "claude-agent-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/1a/1a1c810e7c74075a4766ac0de66e3e510e0267533baa41a089ab1eb5bf01/agent_framework_claude-1.0.0b260507.tar.gz", hash = "sha256:0daccfef8141470fd206bb8b30925a44ba42ec6fb8946934dbcefe50cfeae14c", size = 11618, upload-time = "2026-05-08T00:08:57.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/f8/4977b7d7f1f2ea82c396de07b04f999c58475476722836f3ed0337722495/agent_framework_claude-1.0.0b260507-py3-none-any.whl", hash = "sha256:3ebd1d391b4413512970da62eb5377099ecd66305048594ec5b65cbdf141623f", size = 11588, upload-time = "2026-05-08T00:09:00.32Z" }, +] + [[package]] name = "agent-framework-copilotstudio" version = "1.0.0b260130" @@ -174,23 +199,17 @@ wheels = [ [[package]] name = "agent-framework-core" -version = "1.0.0b260107" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "azure-identity" }, - { name = "mcp", extra = ["ws"] }, - { name = "openai" }, { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "opentelemetry-semantic-conventions-ai" }, - { name = "packaging" }, { name = "pydantic" }, - { name = "pydantic-settings" }, + { name = "python-dotenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/44/06f5d2c99dd7bdb82c2cb5cbc354b5bc6af72d1886d20eff1dff83508fae/agent_framework_core-1.0.0b260107.tar.gz", hash = "sha256:12636fb64664c6153546f0d85dafccdbe57226767c14b3f38985867389f980bb", size = 3574757, upload-time = "2026-01-07T23:57:16.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/59/4c212abdb93074677d643e31a3c21e33ff26a3ccc351145475cd1ffffad7/agent_framework_core-1.3.0.tar.gz", hash = "sha256:91c3659718b733f70dde6fb3626edb044733e0f7aa5f9726c9774e17fae328ef", size = 365395, upload-time = "2026-05-08T00:09:09.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5a/8c6315a2ca119ad48340344616d4b8e77fd68e2892f82c402069a52ad647/agent_framework_core-1.0.0b260107-py3-none-any.whl", hash = "sha256:5bd119b8d30dc2d5bee1c4a5c3597d7afc808a52e4de148725c4f2d9bcc7632b", size = 5687298, upload-time = "2026-01-07T23:57:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/56/f2/c4258333f2691ee10869bf72f51d423808962ccf0c195b1f893c06c348ad/agent_framework_core-1.3.0-py3-none-any.whl", hash = "sha256:b7a5baf2beb383e9042af057df79dae4fda0b836cbc8530b3b2a57a3c12bb7ac", size = 407978, upload-time = "2026-05-08T00:09:32.752Z" }, ] [package.optional-dependencies] @@ -198,18 +217,28 @@ all = [ { name = "agent-framework-a2a" }, { name = "agent-framework-ag-ui" }, { name = "agent-framework-anthropic" }, - { name = "agent-framework-azure-ai" }, { name = "agent-framework-azure-ai-search" }, + { name = "agent-framework-azure-cosmos" }, { name = "agent-framework-azurefunctions" }, + { name = "agent-framework-bedrock" }, { name = "agent-framework-chatkit" }, + { name = "agent-framework-claude" }, { name = "agent-framework-copilotstudio" }, { name = "agent-framework-declarative" }, { name = "agent-framework-devui" }, + { name = "agent-framework-durabletask" }, + { name = "agent-framework-foundry" }, + { name = "agent-framework-foundry-local" }, + { name = "agent-framework-github-copilot" }, + { name = "agent-framework-hyperlight", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32')" }, { name = "agent-framework-lab" }, { name = "agent-framework-mem0" }, { name = "agent-framework-ollama" }, + { name = "agent-framework-openai" }, + { name = "agent-framework-orchestrations" }, { name = "agent-framework-purview" }, { name = "agent-framework-redis" }, + { name = "mcp" }, ] [[package]] @@ -256,6 +285,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/22/122ed515935926137cc3c6ca795ef01b30feb82160cfc0f29a34f9d603de/agent_framework_durabletask-1.0.0b260130-py3-none-any.whl", hash = "sha256:a46e292800d10a62ce0923efe753594ddbf0bd6d1bb6e1258380f0dbf7d0302f", size = 36357, upload-time = "2026-01-30T19:01:24.057Z" }, ] +[[package]] +name = "agent-framework-foundry" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "agent-framework-openai" }, + { name = "azure-ai-inference" }, + { name = "azure-ai-projects" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/f6/8700acd779cbffd933dcb5dc878abce3e0a2f536962567665ccc49965715/agent_framework_foundry-1.3.0.tar.gz", hash = "sha256:8a4b137efa0a7000e60fb396ad90e01c271d14a52f1325f1f0a32177d944bcff", size = 32620, upload-time = "2026-05-08T00:09:04.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/53/9acf5831263d4fcd1d5b8d39af99ee430ec2710d2f9adeab5a1fe7559da0/agent_framework_foundry-1.3.0-py3-none-any.whl", hash = "sha256:49987bc01b077f6c60af33c475f9770a02b4ff6d6822aede18fc5471b46ffd41", size = 37052, upload-time = "2026-05-08T00:09:13.139Z" }, +] + +[[package]] +name = "agent-framework-foundry-local" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "agent-framework-openai" }, + { name = "foundry-local-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/03/8f0b8a2209fd091903bbb068c4458f19c74e48d37f4fa08748d76c3f3091/agent_framework_foundry_local-1.0.0b260507.tar.gz", hash = "sha256:fc2d98ff1f98d0481544c3ad8453f2d56096203fd368d0b68f52ef6ae4c7b0a6", size = 6719, upload-time = "2026-05-08T00:09:35.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/07/1120c862714d89f40d4575a052a495f86bda0fdb4132d5c4597c7a735875/agent_framework_foundry_local-1.0.0b260507-py3-none-any.whl", hash = "sha256:515346ca7716d86c9a4110db9f5586a65c4970ac442aaa00725d27341c5825df", size = 7176, upload-time = "2026-05-08T00:09:28.74Z" }, +] + +[[package]] +name = "agent-framework-github-copilot" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "github-copilot-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/0f/0cab3d20c84ff309f820d02e810c1fa17f1a6fc432775605e34f651955ae/agent_framework_github_copilot-1.0.0b260507.tar.gz", hash = "sha256:f8640d4a18beca67a83b833b5d23f873aa5e1d4e91423ee1923d650b7b97d06d", size = 12546, upload-time = "2026-05-08T00:08:59.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/75/c8747c30acf236daa97063763fd16e443a2734e80c5678c42e103d1b50d6/agent_framework_github_copilot-1.0.0b260507-py3-none-any.whl", hash = "sha256:53a5daae86824fce017f30637edd5e50675e4630da5be09bb259383713198f40", size = 12510, upload-time = "2026-05-08T00:09:42.889Z" }, +] + +[[package]] +name = "agent-framework-hyperlight" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core", marker = "python_full_version < '3.14'" }, + { name = "hyperlight-sandbox", marker = "python_full_version < '3.14'" }, + { name = "hyperlight-sandbox-backend-wasm", marker = "(python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version < '3.14' and platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "hyperlight-sandbox-python-guest", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/1f/52a2541d4a0bc5657ca9c2ef4f85885fb323682052da3fc1451eabafb73d/agent_framework_hyperlight-1.0.0b260507.tar.gz", hash = "sha256:845baab7439ac7b94ee53805cf3d32d0eea3b77a040d0f1b367f0a395fd8c08b", size = 19057, upload-time = "2026-05-08T00:09:56.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/d8/c2e0d3f63ea53f9897bd6c31a3d07c41c48a7b30fd7a1c2b5182fffe32ca/agent_framework_hyperlight-1.0.0b260507-py3-none-any.whl", hash = "sha256:121b464edf32f3db0e5b2891525d8937f0854bc19102a7c50b1905ff29063da7", size = 19589, upload-time = "2026-05-08T00:09:52.71Z" }, +] + [[package]] name = "agent-framework-lab" version = "1.0.0b251024" @@ -294,6 +380,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/27/23e23a1919592dcf2aaf25aa9950a7dbda77c4ba03cba8843491b9f12024/agent_framework_ollama-1.0.0b260130-py3-none-any.whl", hash = "sha256:55e4e17f226ad61e8a9dcbbcc24ab006a3480043ecb4d32c12d2444f628054d6", size = 9167, upload-time = "2026-01-30T19:01:05.647Z" }, ] +[[package]] +name = "agent-framework-openai" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/54/26595b5fa394dd91a5bd434f87b1e7d781545efbf0bd8053de193f89ec63/agent_framework_openai-1.3.0.tar.gz", hash = "sha256:770828447875ee169dde8cd2f2a0343f427d856af7c83895ca12d59f8c24a7f2", size = 49146, upload-time = "2026-05-08T00:09:44.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/d8/a0e0af08123d3c2ff3f42b6976eed155536c73be4d61b898bc15cf31a38c/agent_framework_openai-1.3.0-py3-none-any.whl", hash = "sha256:1953dcb9f3e852362be84b4316ee69639313a7f119eab6ce8c88949e1f24aa4b", size = 54041, upload-time = "2026-05-08T00:09:17.744Z" }, +] + +[[package]] +name = "agent-framework-orchestrations" +version = "1.0.0b260507" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/84/1a26978d91c40f62ef472fd36d1502545bb7425b94b03765c41b322e3398/agent_framework_orchestrations-1.0.0b260507.tar.gz", hash = "sha256:3f17281a2603240e3eed26174cab6b3dca153cb18cec8380f4719e598a55013f", size = 55971, upload-time = "2026-05-08T00:09:37.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/f2df27ba789130470311e7487d19815483f837094672408a22655b33784a/agent_framework_orchestrations-1.0.0b260507-py3-none-any.whl", hash = "sha256:396a5ed962c2a3b1f09d8fc777933397df486bdae0a5f81cf63595c4c6f102de", size = 62074, upload-time = "2026-05-08T00:09:31.24Z" }, +] + [[package]] name = "agent-framework-purview" version = "1.0.0b260130" @@ -804,6 +915,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, ] +[[package]] +name = "boto3" +version = "1.43.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/ad/32ac82224c571776d1119c8d2a5eafeab97bace3b4ed2870cb80d5cda140/boto3-1.43.27.tar.gz", hash = "sha256:dc0d1b47f391983d8b3047e49402d31f9aaa4d7b398d3b4ea986fe680cbea43a", size = 113143, upload-time = "2026-06-10T19:38:35.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/1b/e423f7ed0177f0cc629f9bba39f505ad4a571b1f73c51402e6000bbf453b/boto3-1.43.27-py3-none-any.whl", hash = "sha256:b3eea072c2fdbbdd8c6161f912f603be10c8ec477625926dea8b91a0842a3482", size = 140538, upload-time = "2026-06-10T19:38:34.012Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/4e/db50ef135f1d9ffc85e209a124004a5829d8f12f4a7a0afdf380cb19866d/botocore-1.43.27.tar.gz", hash = "sha256:2093c316c24214e50e18640b1869513b759bb8cc48b95b004a8306cb9f0d6703", size = 15504242, upload-time = "2026-06-10T19:38:25.389Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/46/05b227b34e434b54867c2c942b0bfbbe2fe41789c18bb15ef787d03e9a56/botocore-1.43.27-py3-none-any.whl", hash = "sha256:4976544e652d5a1d8eca135da019f8e1c2d749efa2f9a31a8fb8c76f1895a40b", size = 15190293, upload-time = "2026-06-10T19:38:22.298Z" }, +] + [[package]] name = "cachetools" version = "7.1.1" @@ -982,6 +1121,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] +[[package]] +name = "claude-agent-sdk" +version = "0.1.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "mcp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/dd/2818538efd18ed4ef72d4775efa75bb36cbea0fa418eda51df85ee9c2424/claude_agent_sdk-0.1.48.tar.gz", hash = "sha256:ee294d3f02936c0b826119ffbefcf88c67731cf8c2d2cb7111ccc97f76344272", size = 87375, upload-time = "2026-03-07T00:21:37.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/cf/bbbdee52ee0c63c8709b0ac03ce3c1da5bdc37def5da0eca63363448744f/claude_agent_sdk-0.1.48-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5761ff1d362e0f17c2b1bfd890d1c897f0aa81091e37bbd15b7d06f05ced552d", size = 57559306, upload-time = "2026-03-07T00:21:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/57/d1/2179154b88d4cf6ba1cf6a15066ee8e96257aaeb1330e625e809ba2f28eb/claude_agent_sdk-0.1.48-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:39c1307daa17e42fa8a71180bb20af8a789d72d3891fc93519ff15540badcb83", size = 73980309, upload-time = "2026-03-07T00:21:24.592Z" }, + { url = "https://files.pythonhosted.org/packages/dc/99/55b0cd3bf54a7449e744d23cf50be104e9445cf623e1ed75722112aa6264/claude_agent_sdk-0.1.48-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:543d70acba468eccfff836965a14b8ac88cf90809aeeb88431dfcea3ee9a2fa9", size = 74583686, upload-time = "2026-03-07T00:21:28.969Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/4851bd9a238b7aadba7639eb906aca7da32a51f01563fa4488469c608b3a/claude_agent_sdk-0.1.48-py3-none-win_amd64.whl", hash = "sha256:0d37e60bd2b17efc3f927dccef080f14897ab62cd1d0d67a4abc8a0e2d4f1006", size = 74956045, upload-time = "2026-03-07T00:21:33.475Z" }, +] + [[package]] name = "click" version = "8.3.3" @@ -1242,6 +1397,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] +[[package]] +name = "foundry-local-sdk" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/6b/76a7fe8f9f4c52cc84eaa1cd1b66acddf993496d55d6ea587bf0d0854d1c/foundry_local_sdk-0.5.1-py3-none-any.whl", hash = "sha256:f3639a3666bc3a94410004a91671338910ac2e1b8094b1587cc4db0f4a7df07e", size = 14003, upload-time = "2025-11-21T05:39:58.099Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -1344,6 +1512,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/8c/dce3b1b7593858eba995b2dfdb833f872c7f863e3da92aab7128a6b11af4/furl-2.1.4-py2.py3-none-any.whl", hash = "sha256:da34d0b34e53ffe2d2e6851a7085a05d96922b5b578620a37377ff1dbeeb11c8", size = 27550, upload-time = "2025-03-09T05:36:19.928Z" }, ] +[[package]] +name = "github-copilot-sdk" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dateutil" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/fe/2cb98d4b9f57f8062ea72775bde72aed1958305016753f7296398e0ceb45/github_copilot_sdk-1.0.0b2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:1b5941d8b6e3d94d42a5bec6607a26f562e6535d5c981089d23d3d224b94601c", size = 67061619, upload-time = "2026-05-06T20:02:08.636Z" }, + { url = "https://files.pythonhosted.org/packages/57/45/76567821b2d36f81e6bca78c98d265e2762733f765fa51d69602b7f81867/github_copilot_sdk-1.0.0b2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b8f6a087a0cf02bb0d33976e8f8c009578d84d701a0b28d52051304791ac70", size = 63790955, upload-time = "2026-05-06T20:02:12.354Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/684b0da0b1207a2bdf025c22ee075d34a1736d61a4973651035d4fd4d8dc/github_copilot_sdk-1.0.0b2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f403638c11b82bddb81c94675fc4e8014a1bb2e86a679a39fa167dcc3ad5416a", size = 69538664, upload-time = "2026-05-06T20:02:16.363Z" }, + { url = "https://files.pythonhosted.org/packages/57/1d/80d88ecf83683535d1a16d4817f1683db3b125f52a924ebdfe9764f5e4c3/github_copilot_sdk-1.0.0b2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:433d16bb31171fee8d3a5b70259c527f63b297e83a8f8761ae1f16f14d641f32", size = 68163648, upload-time = "2026-05-06T20:02:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/32/d3/b72aa2fbb3194b50b53e8cb1484f5606a1f8eedcdb0bfb5747da52079553/github_copilot_sdk-1.0.0b2-py3-none-win_amd64.whl", hash = "sha256:a6e9782dae4c3c2ab3527b45bb5de0f61998104c10e9ff64698280eaf37ab5dd", size = 62649144, upload-time = "2026-05-06T20:02:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e2/be95b8ea0ac11d1ca474e28a59284f4e395c2710734eadfb657f5de8ace2/github_copilot_sdk-1.0.0b2-py3-none-win_arm64.whl", hash = "sha256:2e97d0ce4bad67dc5929091cb429e7bbae7d4643e4908a6af256a41439000740", size = 60374365, upload-time = "2026-05-06T20:02:29.02Z" }, +] + [[package]] name = "google-api-core" version = "2.30.3" @@ -1546,6 +1731,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, ] +[[package]] +name = "hyperlight-sandbox" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/5e/14c69eac7e1c74fbd556c6f890729a3d232d32d65cd9f8cfde72c0534e61/hyperlight_sandbox-0.4.0.tar.gz", hash = "sha256:90d7b91d4d8e17054e282b0daed55c261392a748dafc57e6416d3184cdac910b", size = 9262, upload-time = "2026-05-02T00:00:02.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/e3/b8c106a274c08a30261105afa5511e0ec55960e86b2f6c51e3095e96647c/hyperlight_sandbox-0.4.0-py3-none-any.whl", hash = "sha256:7ae44d2448ed6ecadb368373c7e45eb395521e7774c86a1cbc1ef9cdfc25cd2a", size = 5723, upload-time = "2026-05-02T00:00:03.811Z" }, +] + +[[package]] +name = "hyperlight-sandbox-backend-wasm" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/e5/3cdf21594eb28de7ca1a5a1ade27e137c8f3d7ab48d65fed87a3b74c4039/hyperlight_sandbox_backend_wasm-0.4.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ff4627950708909202ee24c6175dc41e9c05479f89393575e3de0f14e6f5a193", size = 3918189, upload-time = "2026-05-01T23:59:16.666Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/b1bb9893bbeb979d133dc542520125dcbf8394d1a2537e753118b37c7cab/hyperlight_sandbox_backend_wasm-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cce7dc28b9ded034a11a9a8cf7b9ffb838e29006be8d2e01646dd131ba501b73", size = 3383520, upload-time = "2026-05-01T23:59:27.261Z" }, + { url = "https://files.pythonhosted.org/packages/8c/29/deee4e31086628750f0ce1f67da1e28c613fd2df68465de130cbfe51e72d/hyperlight_sandbox_backend_wasm-0.4.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:88e194515e4784f68676b6906c98a4000f913c93172cf07981d8a977e756bbd6", size = 3917939, upload-time = "2026-05-01T23:59:14.805Z" }, + { url = "https://files.pythonhosted.org/packages/15/2a/6822aec3c04c46893406d0d6ed576dbdb4b5c1d76a0124dc220bb45b0d34/hyperlight_sandbox_backend_wasm-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d1cd2269a5651ea9be1f94a3e3388f6af69e41dbc2b808c3b806481fe17ce163", size = 3383110, upload-time = "2026-05-01T23:59:23.736Z" }, +] + +[[package]] +name = "hyperlight-sandbox-python-guest" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fd/816d1f3f277ff149a45da5381967aa04c22bc7702b5c14f0acfd9db2cee7/hyperlight_sandbox_python_guest-0.4.0.tar.gz", hash = "sha256:64c3c6c13fe550bf5b680fa0b965cf62bc4668084cc275c3467e3c015e6ead36", size = 21657381, upload-time = "2026-05-01T23:59:46.589Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/ba/efb9aacf993f0ac142da5beb9177b221e49dc860c6ea398de236015a52a0/hyperlight_sandbox_python_guest-0.4.0-py3-none-any.whl", hash = "sha256:0789eb794b99606288402ed3921b5e2630800a69d24117ecd9b82e816568202d", size = 21822062, upload-time = "2026-05-01T23:59:50.99Z" }, +] + [[package]] name = "identify" version = "2.6.19" @@ -1720,6 +1934,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "joserfc" version = "1.6.4" @@ -1926,11 +2149,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, ] -[package.optional-dependencies] -ws = [ - { name = "websockets" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -2359,19 +2577,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, ] -[[package]] -name = "opentelemetry-semantic-conventions-ai" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-sdk" }, - { name = "opentelemetry-semantic-conventions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/02/10aeacc37a38a3a8fa16ff67bec1ae3bf882539f6f9efb0f70acf802ca2d/opentelemetry_semantic_conventions_ai-0.5.1.tar.gz", hash = "sha256:153906200d8c1d2f8e09bd78dbef526916023de85ac3dab35912bfafb69ff04c", size = 26533, upload-time = "2026-03-26T14:20:38.73Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/22/41fb05f1dc5fda2c468e05a41814c20859016c85117b66c8a257cae814f6/opentelemetry_semantic_conventions_ai-0.5.1-py3-none-any.whl", hash = "sha256:25aeb22bd261543b4898a73824026d96770e5351209c7d07a0b1314762b1f6e4", size = 11250, upload-time = "2026-03-26T14:20:37.108Z" }, -] - [[package]] name = "orderedmultidict" version = "1.0.2" @@ -2523,7 +2728,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "agent-framework", specifier = "==1.0.0b260107" }, + { name = "agent-framework", specifier = "==1.3.0" }, { name = "aiohttp", specifier = "==3.13.5" }, { name = "art", specifier = "==6.5" }, { name = "azure-ai-agents", specifier = "==1.2.0b5" }, @@ -3269,6 +3474,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, ] +[[package]] +name = "s3transfer" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/1f/12417f7f493fc45e1f9fd5d4a9b6c125cf8d2cf3f8ddbdfab3e76406e9d6/s3transfer-0.18.0.tar.gz", hash = "sha256:3760b8b7ec1315da54048b2d626276732bee4300d054d492d4e1d43e20d4ecbd", size = 160560, upload-time = "2026-05-28T19:39:09.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/58/a58fc997655386daa2e25784e30c288aa3e3819e401f77029ee4899fb55a/s3transfer-0.18.0-py3-none-any.whl", hash = "sha256:239c13b09e65ad0346e1be7348b8a202dcad44ac7ea7c6eb858fc881dce739b6", size = 88572, upload-time = "2026-05-28T19:39:07.999Z" }, +] + [[package]] name = "sas-cosmosdb" version = "0.1.5" From e67884a951cda7f7cd94ec9899708790042b0b29 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 12:27:56 +0530 Subject: [PATCH 14/50] Fix SharedMemoryContextProvider not iterable TypeError AgentBuilder.with_context_providers() and with_middleware() accepted single objects but passed them directly to Agent(), which expects Sequence types. Now both methods auto-wrap single items into a list. Also wrapped the call site in orchestrator_base.py for clarity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/libs/agent_framework/agent_builder.py | 10 ++++++++-- src/processor/src/libs/base/orchestrator_base.py | 2 +- .../libs/agent_framework/test_agent_builder.py | 14 ++++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/processor/src/libs/agent_framework/agent_builder.py b/src/processor/src/libs/agent_framework/agent_builder.py index 888e2dae..65261b9b 100644 --- a/src/processor/src/libs/agent_framework/agent_builder.py +++ b/src/processor/src/libs/agent_framework/agent_builder.py @@ -266,7 +266,10 @@ def with_middleware( Returns: Self for method chaining """ - self._middleware = middleware + if isinstance(middleware, list): + self._middleware = middleware + else: + self._middleware = [middleware] return self def with_context_providers( @@ -281,7 +284,10 @@ def with_context_providers( Returns: Self for method chaining """ - self._context_providers = context_providers + if isinstance(context_providers, list): + self._context_providers = context_providers + else: + self._context_providers = [context_providers] return self def with_conversation_id(self, conversation_id: str) -> "AgentBuilder": diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index a4570c04..9e198f9f 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -209,7 +209,7 @@ async def create_agents( agent_name=agent_info.agent_name, step=self.step_name, ) - builder = builder.with_context_providers(memory_provider) + builder = builder.with_context_providers([memory_provider]) agent = builder.build() agents[agent_info.agent_name] = agent diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py b/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py index 572974a8..f1a86cb5 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_agent_builder.py @@ -57,12 +57,22 @@ def test_with_tool_choice(self): def test_with_middleware(self): m = [MagicMock()] b = _builder().with_middleware(m) - assert b._middleware is m + assert b._middleware == m + + def test_with_middleware_single(self): + m = MagicMock() + b = _builder().with_middleware(m) + assert b._middleware == [m] def test_with_context_providers(self): cp = MagicMock() b = _builder().with_context_providers(cp) - assert b._context_providers is cp + assert b._context_providers == [cp] + + def test_with_context_providers_list(self): + cp1, cp2 = MagicMock(), MagicMock() + b = _builder().with_context_providers([cp1, cp2]) + assert b._context_providers == [cp1, cp2] def test_with_conversation_id(self): b = _builder().with_conversation_id("conv-1") From e3647936b34b30e48491b2c0624e4c74aa1dea1d Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 12:31:16 +0530 Subject: [PATCH 15/50] Remove trailing blank line in azure_openai_response_retry.py (W391) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/libs/agent_framework/azure_openai_response_retry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 3fbd0e8b..4809162b 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -609,5 +609,4 @@ async def _inner_get_response( messages=trimmed, options=options, **kwargs ), config=self._retry_config, - ) - + ) From fa63bdfbdd5f025fab7d8db2894671d899ef5466 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 15:19:26 +0530 Subject: [PATCH 16/50] Fix async for coroutine error in retry client streaming path The parent OpenAIChatClient._inner_get_response is a regular def that returns ResponseStream (async iterable) when stream=True, or Awaitable when stream=False. The override was async def, which always returned a coroutine, breaking 'async for event in workflow.run(stream=True)'. Refactored to: - Regular def _inner_get_response dispatching stream vs non-stream - _non_streaming_with_retry: async coroutine with retry + context-trim - _streaming_with_retry: async generator with pre-first-chunk retry - _maybe_trim_messages: shared context-trim helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure_openai_response_retry.py | 183 ++++++++++++++---- 1 file changed, 149 insertions(+), 34 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 4809162b..0370f389 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -10,7 +10,7 @@ import os import random from dataclasses import dataclass -from typing import Any, MutableSequence +from typing import Any, AsyncIterable, MutableSequence from agent_framework.openai import OpenAIChatClient from tenacity import ( @@ -530,35 +530,72 @@ def __init__( self._retry_config = retry_config or RateLimitRetryConfig.from_env() self._context_trim_config = ContextTrimConfig.from_env() - async def _inner_get_response( - self, *, messages: MutableSequence[Any], options: Any = None, **kwargs: Any + def _inner_get_response( + self, *, messages: MutableSequence[Any], options: Any = None, stream: bool = False, **kwargs: Any ) -> Any: - parent_inner_get_response = super( + """Override that adds retry + context-trimming around the parent call. + + Must remain a regular ``def`` (not ``async def``) because the parent + returns different types depending on *stream*: + - stream=False → Awaitable[ChatResponse] + - stream=True → ResponseStream (AsyncIterable) + """ + effective_messages = self._maybe_trim_messages(messages) + + if stream: + return self._streaming_with_retry( + effective_messages=effective_messages, + original_messages=messages, + options=options, + **kwargs, + ) + else: + return self._non_streaming_with_retry( + effective_messages=effective_messages, + original_messages=messages, + options=options, + **kwargs, + ) + + def _maybe_trim_messages( + self, messages: MutableSequence[Any] + ) -> MutableSequence[Any] | list[Any]: + """Apply pre-call context trimming if enabled and over budget.""" + if not self._context_trim_config.enabled: + return messages + approx_chars = sum(len(_estimate_message_text(m)) for m in messages) + if ( + self._context_trim_config.max_total_chars > 0 + and approx_chars > self._context_trim_config.max_total_chars + ): + trimmed = _trim_messages(messages, cfg=self._context_trim_config) + logger.warning( + "[AOAI_CTX_TRIM] pre-trimmed request messages: approx_chars=%s -> %s; count=%s -> %s", + approx_chars, + sum(len(_estimate_message_text(m)) for m in trimmed), + len(messages), + len(trimmed), + ) + return trimmed + return messages + + async def _non_streaming_with_retry( + self, + *, + effective_messages: MutableSequence[Any] | list[Any], + original_messages: MutableSequence[Any], + options: Any = None, + **kwargs: Any, + ) -> Any: + """Non-streaming path: full retry + context-trim fallback.""" + parent_inner = super( AzureOpenAIResponseClientWithRetry, self )._inner_get_response - effective_messages: MutableSequence[Any] | list[Any] = messages - if self._context_trim_config.enabled: - approx_chars = sum(len(_estimate_message_text(m)) for m in messages) - if ( - self._context_trim_config.max_total_chars > 0 - and approx_chars > self._context_trim_config.max_total_chars - ): - effective_messages = _trim_messages( - messages, cfg=self._context_trim_config - ) - logger.warning( - "[AOAI_CTX_TRIM] pre-trimmed request messages: approx_chars=%s -> %s; count=%s -> %s", - approx_chars, - sum(len(_estimate_message_text(m)) for m in effective_messages), - len(messages), - len(effective_messages), - ) - try: return await _retry_call( - lambda: parent_inner_get_response( - messages=effective_messages, options=options, **kwargs + lambda: parent_inner( + messages=effective_messages, options=options, stream=False, **kwargs ), config=self._retry_config, ) @@ -571,7 +608,7 @@ async def _inner_get_response( raise trimmed = _trim_messages( - messages, + original_messages, cfg=ContextTrimConfig( enabled=True, max_total_chars=max( @@ -593,20 +630,98 @@ async def _inner_get_response( ) logger.warning( "[AOAI_CTX_TRIM] retrying after context-length error; count=%s -> %s", - len(messages), + len(original_messages), len(trimmed), ) - # Cool down before retrying to avoid triggering 429s immediately. - trim_delay = self._retry_config.base_delay_seconds - trim_delay = min(trim_delay, self._retry_config.max_delay_seconds) + trim_delay = min( + self._retry_config.base_delay_seconds, + self._retry_config.max_delay_seconds, + ) logger.info( - "[AOAI_CTX_TRIM] sleeping %ss before retry", - round(trim_delay, 1), + "[AOAI_CTX_TRIM] sleeping %ss before retry", round(trim_delay, 1) ) await asyncio.sleep(trim_delay) return await _retry_call( - lambda: parent_inner_get_response( - messages=trimmed, options=options, **kwargs + lambda: parent_inner( + messages=trimmed, options=options, stream=False, **kwargs ), config=self._retry_config, - ) + ) + + async def _streaming_with_retry( + self, + *, + effective_messages: MutableSequence[Any] | list[Any], + original_messages: MutableSequence[Any], + options: Any = None, + **kwargs: Any, + ) -> AsyncIterable[Any]: + """Streaming path: retry only before the first chunk is yielded.""" + parent_inner = super( + AzureOpenAIResponseClientWithRetry, self + )._inner_get_response + + attempts = self._retry_config.max_retries + 1 + + for attempt_index in range(attempts): + response_stream = parent_inner( + messages=effective_messages, options=options, stream=True, **kwargs + ) + + iterator = response_stream.__aiter__() + try: + first = await iterator.__anext__() + + async def _tail(): + yield first + async for item in iterator: + yield item + + async for item in _tail(): + yield item + return + except StopAsyncIteration: + return + except Exception as e: + close = getattr(response_stream, "aclose", None) + if callable(close): + try: + await close() + except Exception: + logger.debug( + "Best-effort close of response stream failed", + exc_info=True, + ) + + if not _looks_like_rate_limit(e) or attempt_index >= attempts - 1: + if _looks_like_rate_limit(e): + logger.warning( + "[AOAI_RETRY_STREAM] giving up after %s/%s attempts; error=%s", + attempt_index + 1, + attempts, + _format_exc_brief(e), + ) + raise + + retry_after = _try_get_retry_after_seconds(e) + if retry_after is not None and retry_after >= 0: + delay = retry_after + else: + delay = self._retry_config.base_delay_seconds * ( + 2**attempt_index + ) + delay = min(delay, self._retry_config.max_delay_seconds) + delay = delay + random.uniform(0.0, 0.25 * max(delay, 0.1)) + + status = getattr(e, "status_code", None) or getattr( + e, "status", None + ) + logger.warning( + "[AOAI_RETRY_STREAM] attempt %s/%s; sleeping=%ss; retry_after=%s; status=%s; error=%s", + attempt_index + 1, + attempts, + round(float(delay), 3), + None if retry_after is None else round(float(retry_after), 3), + status, + _format_exc_brief(e), + ) From 0f10ef03a52b1348ddaec299a3c688bb1270502c Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 15:53:19 +0530 Subject: [PATCH 17/50] Fix streaming: delegate to parent ResponseStream instead of async generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The framework's BaseChatClient.get_response checks isinstance(result, ResponseStream) for streaming responses. Our async generator from _streaming_with_retry failed that check, causing the framework to 'await' it — which fails with 'object async_generator can't be used in await expression'. Fix: for streaming, pass through to the parent's _inner_get_response which returns a proper ResponseStream. Retry is preserved for non-streaming calls. Removed unused _streaming_with_retry method. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure_openai_response_retry.py | 93 ++----------------- 1 file changed, 9 insertions(+), 84 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 0370f389..f077532b 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -10,7 +10,7 @@ import os import random from dataclasses import dataclass -from typing import Any, AsyncIterable, MutableSequence +from typing import Any, MutableSequence from agent_framework.openai import OpenAIChatClient from tenacity import ( @@ -543,11 +543,14 @@ def _inner_get_response( effective_messages = self._maybe_trim_messages(messages) if stream: - return self._streaming_with_retry( - effective_messages=effective_messages, - original_messages=messages, - options=options, - **kwargs, + # For streaming, delegate to the parent which returns a proper + # ResponseStream. The framework checks isinstance(result, ResponseStream) + # and async generators fail that check. + parent_inner = super( + AzureOpenAIResponseClientWithRetry, self + )._inner_get_response + return parent_inner( + messages=effective_messages, options=options, stream=True, **kwargs ) else: return self._non_streaming_with_retry( @@ -647,81 +650,3 @@ async def _non_streaming_with_retry( ), config=self._retry_config, ) - - async def _streaming_with_retry( - self, - *, - effective_messages: MutableSequence[Any] | list[Any], - original_messages: MutableSequence[Any], - options: Any = None, - **kwargs: Any, - ) -> AsyncIterable[Any]: - """Streaming path: retry only before the first chunk is yielded.""" - parent_inner = super( - AzureOpenAIResponseClientWithRetry, self - )._inner_get_response - - attempts = self._retry_config.max_retries + 1 - - for attempt_index in range(attempts): - response_stream = parent_inner( - messages=effective_messages, options=options, stream=True, **kwargs - ) - - iterator = response_stream.__aiter__() - try: - first = await iterator.__anext__() - - async def _tail(): - yield first - async for item in iterator: - yield item - - async for item in _tail(): - yield item - return - except StopAsyncIteration: - return - except Exception as e: - close = getattr(response_stream, "aclose", None) - if callable(close): - try: - await close() - except Exception: - logger.debug( - "Best-effort close of response stream failed", - exc_info=True, - ) - - if not _looks_like_rate_limit(e) or attempt_index >= attempts - 1: - if _looks_like_rate_limit(e): - logger.warning( - "[AOAI_RETRY_STREAM] giving up after %s/%s attempts; error=%s", - attempt_index + 1, - attempts, - _format_exc_brief(e), - ) - raise - - retry_after = _try_get_retry_after_seconds(e) - if retry_after is not None and retry_after >= 0: - delay = retry_after - else: - delay = self._retry_config.base_delay_seconds * ( - 2**attempt_index - ) - delay = min(delay, self._retry_config.max_delay_seconds) - delay = delay + random.uniform(0.0, 0.25 * max(delay, 0.1)) - - status = getattr(e, "status_code", None) or getattr( - e, "status", None - ) - logger.warning( - "[AOAI_RETRY_STREAM] attempt %s/%s; sleeping=%ss; retry_after=%s; status=%s; error=%s", - attempt_index + 1, - attempts, - round(float(delay), 3), - None if retry_after is None else round(float(retry_after), 3), - status, - _format_exc_brief(e), - ) From 688b136f362cf79bce321c5de412c3141c9c514f Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 17:14:59 +0530 Subject: [PATCH 18/50] Guard against empty messages after context trimming - _trim_messages: keep at least 1 message (never pop to empty) - _maybe_trim_messages: fall back to originals if trim produces empty - _non_streaming_with_retry: re-raise if aggressive trim empties list - _inner_get_response: log warning and use originals if messages empty Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure_openai_response_retry.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index f077532b..1df1a7cc 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -391,13 +391,14 @@ def _trim_messages( def _total_chars(msgs: list[Any]) -> int: return sum(len(_estimate_message_text(x)) for x in msgs) - while combined and _total_chars(combined) > cfg.max_total_chars: + while len(combined) > 1 and _total_chars(combined) > cfg.max_total_chars: # Prefer dropping earliest non-system message. + # Never drop the last message — the model needs at least one. drop_index = 0 if cfg.keep_system_messages and system_messages: drop_index = len(system_messages) - if drop_index >= len(combined): - # If only system messages remain, truncate the last one. + if drop_index >= len(combined) - 1: + # Only system messages (+ maybe 1 non-system) remain — truncate the last one. last = combined[-1] text = _estimate_message_text(last) text = _truncate_text( @@ -542,6 +543,12 @@ def _inner_get_response( """ effective_messages = self._maybe_trim_messages(messages) + if not effective_messages: + logger.warning( + "[AOAI_RETRY] empty messages list received; using original messages" + ) + effective_messages = messages + if stream: # For streaming, delegate to the parent which returns a proper # ResponseStream. The framework checks isinstance(result, ResponseStream) @@ -572,6 +579,11 @@ def _maybe_trim_messages( and approx_chars > self._context_trim_config.max_total_chars ): trimmed = _trim_messages(messages, cfg=self._context_trim_config) + if not trimmed: + logger.warning( + "[AOAI_CTX_TRIM] trimming would remove all messages; keeping originals" + ) + return messages logger.warning( "[AOAI_CTX_TRIM] pre-trimmed request messages: approx_chars=%s -> %s; count=%s -> %s", approx_chars, @@ -631,6 +643,11 @@ async def _non_streaming_with_retry( retry_on_context_error=True, ), ) + if not trimmed: + logger.warning( + "[AOAI_CTX_TRIM] aggressive trim would remove all messages; re-raising original error" + ) + raise logger.warning( "[AOAI_CTX_TRIM] retrying after context-length error; count=%s -> %s", len(original_messages), From 1d8617632fba353536960cf46a2431830ed5fa20 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 14:32:46 +0530 Subject: [PATCH 19/50] Update AZURE_OPENAI_API_VERSION from 2025-03-01-preview to v1 The Responses API requires the new v1 API endpoint. The old preview version (2025-03-01-preview) does not support the /responses endpoint, causing BadRequest 'API version not supported' errors at runtime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- infra/main.bicep | 2 +- infra/main.json | 2 +- infra/main_custom.bicep | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index f7a08659..456e3168 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1135,7 +1135,7 @@ module appConfiguration 'br/public:avm/res/app-configuration/configuration-store } { name: 'AZURE_OPENAI_API_VERSION' - value: '2025-03-01-preview' + value: 'v1' } { name: 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' diff --git a/infra/main.json b/infra/main.json index 67cbf372..b34fdd30 100644 --- a/infra/main.json +++ b/infra/main.json @@ -35380,7 +35380,7 @@ }, { "name": "AZURE_OPENAI_API_VERSION", - "value": "2025-03-01-preview" + "value": "v1" }, { "name": "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index a4588fb5..6dd44b9b 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -1101,7 +1101,7 @@ module appConfiguration 'br/public:avm/res/app-configuration/configuration-store } { name: 'AZURE_OPENAI_API_VERSION' - value: '2025-03-01-preview' + value: 'v1' } { name: 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' From 6938b2b49e9a64bf972147dced0b0d4ce7c8c6e3 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 21:35:54 +0530 Subject: [PATCH 20/50] Revert "Update AZURE_OPENAI_API_VERSION from 2025-03-01-preview to v1" This reverts commit 1d8617632fba353536960cf46a2431830ed5fa20. --- infra/main.bicep | 2 +- infra/main.json | 2 +- infra/main_custom.bicep | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 456e3168..f7a08659 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1135,7 +1135,7 @@ module appConfiguration 'br/public:avm/res/app-configuration/configuration-store } { name: 'AZURE_OPENAI_API_VERSION' - value: 'v1' + value: '2025-03-01-preview' } { name: 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' diff --git a/infra/main.json b/infra/main.json index b34fdd30..67cbf372 100644 --- a/infra/main.json +++ b/infra/main.json @@ -35380,7 +35380,7 @@ }, { "name": "AZURE_OPENAI_API_VERSION", - "value": "v1" + "value": "2025-03-01-preview" }, { "name": "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 6dd44b9b..a4588fb5 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -1101,7 +1101,7 @@ module appConfiguration 'br/public:avm/res/app-configuration/configuration-store } { name: 'AZURE_OPENAI_API_VERSION' - value: 'v1' + value: '2025-03-01-preview' } { name: 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' From 59618707a9f852004baadcb33f1bb10fb587216e Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 21:37:57 +0530 Subject: [PATCH 21/50] Switch default client to AzureOpenAIChatCompletionWithRetry (Chat Completions API) Adds a new AzureOpenAIChatClientWithRetry that wraps OpenAIChatCompletionClient (the /chat/completions endpoint) with the same 429-retry and context-trimming logic as the existing AzureOpenAIResponseClientWithRetry, then switches the default client registered in AgentFrameworkHelper and the per-thread client in OrchestratorBase to use it. The /chat/completions endpoint works with the existing 2025-03-01-preview Azure OpenAI API version, so the v1 API-version bump (commit 1d86176) is no longer required and is reverted in the prior commit. Mirrors the approach used in microsoft/content-processing-solution-accelerator#599. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/agent_framework_helper.py | 42 ++++++++- .../azure_openai_response_retry.py | 94 ++++++++++++++++++- .../src/libs/base/orchestrator_base.py | 2 +- .../test_agent_framework_helper.py | 16 ++++ 4 files changed, 151 insertions(+), 3 deletions(-) diff --git a/src/processor/src/libs/agent_framework/agent_framework_helper.py b/src/processor/src/libs/agent_framework/agent_framework_helper.py index b1bf6ba8..4c90b574 100644 --- a/src/processor/src/libs/agent_framework/agent_framework_helper.py +++ b/src/processor/src/libs/agent_framework/agent_framework_helper.py @@ -22,6 +22,7 @@ from .agent_framework_settings import AgentFrameworkSettings from .azure_openai_response_retry import ( + AzureOpenAIChatClientWithRetry, AzureOpenAIResponseClientWithRetry, RateLimitRetryConfig, ) @@ -40,6 +41,7 @@ class ClientType(Enum): OpenAIAssistant = "OpenAIAssistant" OpenAIResponse = "OpenAIResponse" AzureOpenAIChatCompletion = "AzureOpenAIChatCompletion" + AzureOpenAIChatCompletionWithRetry = "AzureOpenAIChatCompletionWithRetry" AzureOpenAIAssistant = "AzureOpenAIAssistant" AzureOpenAIResponse = "AzureOpenAIResponse" AzureOpenAIResponseWithRetry = "AzureOpenAIResponseWithRetry" @@ -92,7 +94,7 @@ def _initialize_all_clients(self, settings: AgentFrameworkSettings): continue self.ai_clients[service_id] = AgentFrameworkHelper.create_client( - client_type=ClientType.AzureOpenAIResponseWithRetry, + client_type=ClientType.AzureOpenAIChatCompletionWithRetry, endpoint=service_config.endpoint, deployment_name=service_config.chat_deployment_name, api_version=service_config.api_version, @@ -148,6 +150,29 @@ def create_client( ) -> "OpenAIChatCompletionClient": pass + @overload + @staticmethod + def create_client( + client_type: type[ClientType.AzureOpenAIChatCompletionWithRetry], + *, + api_key: str | None = None, + deployment_name: str | None = None, + endpoint: str | None = None, + base_url: str | None = None, + api_version: str | None = None, + ad_token: str | None = None, + ad_token_provider: object | None = None, + token_endpoint: str | None = None, + credential: object | None = None, + default_headers: dict[str, str] | None = None, + async_client: object | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + instruction_role: str | None = None, + retry_config: RateLimitRetryConfig | None = None, + ) -> AzureOpenAIChatClientWithRetry: + pass + @overload @staticmethod def create_client( @@ -379,6 +404,21 @@ def create_client( env_file_encoding=env_file_encoding, instruction_role=instruction_role, ) + elif client_type == ClientType.AzureOpenAIChatCompletionWithRetry: + return AzureOpenAIChatClientWithRetry( + model=deployment_name, + api_key=api_key, + azure_endpoint=endpoint, + base_url=base_url, + api_version=api_version, + credential=credential or ad_token_provider, + default_headers=default_headers, + async_client=async_client, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + instruction_role=instruction_role, + retry_config=retry_config, + ) elif client_type == ClientType.AzureOpenAIAssistant: raise NotImplementedError( "AzureOpenAIAssistantsClient has been removed in agent-framework 1.3.0. " diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 1df1a7cc..9eff88e2 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -12,7 +12,7 @@ from dataclasses import dataclass from typing import Any, MutableSequence -from agent_framework.openai import OpenAIChatClient +from agent_framework.openai import OpenAIChatClient, OpenAIChatCompletionClient from tenacity import ( AsyncRetrying, retry_if_exception, @@ -667,3 +667,95 @@ async def _non_streaming_with_retry( ), config=self._retry_config, ) + + +class AzureOpenAIChatClientWithRetry(OpenAIChatCompletionClient): + """Azure OpenAI Chat (Chat Completions) client with 429 retry at the request boundary. + + Wraps the ``/chat/completions`` endpoint used by Agent Framework by overriding + the internal ``_inner_get_response`` method. This client works with all Azure + OpenAI API versions including ``2025-03-01-preview``. + + Use this in preference to ``AzureOpenAIResponseClientWithRetry`` when the + ``/responses`` endpoint (and the ``v1`` API version it requires) is not + available in the target Azure OpenAI resource. + """ + + def __init__( + self, + *args: Any, + retry_config: RateLimitRetryConfig | None = None, + **kwargs: Any, + ): + super().__init__(*args, **kwargs) + self._retry_config = retry_config or RateLimitRetryConfig.from_env() + self._context_trim_config = ContextTrimConfig.from_env() + + async def _inner_get_response( + self, + *, + messages: MutableSequence[Any], + options: Any | None = None, + **kwargs: Any, + ) -> Any: + """Override that adds retry + context-trimming around the parent call.""" + parent_inner_get_response = super( + AzureOpenAIChatClientWithRetry, self + )._inner_get_response + + effective_messages: MutableSequence[Any] | list[Any] = messages + if self._context_trim_config.enabled: + approx_chars = sum(len(_estimate_message_text(m)) for m in messages) + if ( + self._context_trim_config.max_total_chars > 0 + and approx_chars > self._context_trim_config.max_total_chars + ): + effective_messages = _trim_messages( + messages, cfg=self._context_trim_config + ) + logger.warning( + "[AOAI_CTX_TRIM] pre-trimmed chat request messages: approx_chars=%s -> %s; count=%s -> %s", + approx_chars, + sum(len(_estimate_message_text(m)) for m in effective_messages), + len(messages), + len(effective_messages), + ) + + if not effective_messages: + logger.warning( + "[AOAI_RETRY] empty messages list received; using original messages" + ) + effective_messages = messages + + try: + return await _retry_call( + lambda: parent_inner_get_response( + messages=effective_messages, options=options, **kwargs + ), + config=self._retry_config, + ) + except Exception as e: + if not ( + self._context_trim_config.enabled + and self._context_trim_config.retry_on_context_error + and _looks_like_context_length(e) + ): + raise + + trimmed = _trim_messages(messages, cfg=self._context_trim_config) + if not trimmed: + logger.warning( + "[AOAI_CTX_TRIM] trim would remove all messages; re-raising original error" + ) + raise + logger.warning( + "[AOAI_CTX_TRIM] retrying chat after context-length error; count=%s -> %s", + len(messages), + len(trimmed), + ) + return await _retry_call( + lambda: parent_inner_get_response( + messages=trimmed, options=options, **kwargs + ), + config=self._retry_config, + ) diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index 9e198f9f..f21473e1 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -225,7 +225,7 @@ async def get_client(self, thread_id: str = None): return self._client_cache[thread_id] else: client = self.agent_framework_helper.create_client( - client_type=ClientType.AzureOpenAIResponseWithRetry, + client_type=ClientType.AzureOpenAIChatCompletionWithRetry, endpoint=self.agent_framework_helper.settings.get_service_config( "default" ).endpoint, diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py b/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py index 578a79d5..ad42b867 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_agent_framework_helper.py @@ -95,6 +95,22 @@ def test_azure_openai_response_with_retry(self): assert kwargs["model"] == "gpt-4" assert kwargs["credential"] == "token" + def test_azure_openai_chat_completion_with_retry(self): + with patch( + "libs.agent_framework.agent_framework_helper.AzureOpenAIChatClientWithRetry" + ) as mock_cls: + client = AgentFrameworkHelper.create_client( + ClientType.AzureOpenAIChatCompletionWithRetry, + endpoint="https://x", + deployment_name="gpt-4", + ad_token_provider="token", + ) + assert client is mock_cls.return_value + kwargs = mock_cls.call_args.kwargs + assert kwargs["azure_endpoint"] == "https://x" + assert kwargs["model"] == "gpt-4" + assert kwargs["credential"] == "token" + def test_default_token_provider_when_no_credential(self): with patch( "libs.agent_framework.agent_framework_helper.AzureOpenAIResponseClientWithRetry" From 3ab3076274bf9b94e20310b1e2e2fb3632e0bf3c Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 21:58:28 +0530 Subject: [PATCH 22/50] Fix AzureOpenAIChatClientWithRetry streaming: sync def + stream parameter OpenAIChatCompletionClient._inner_get_response is a SYNC method that returns either Awaitable[ChatResponse] (stream=False) or ResponseStream (stream=True), matching the OpenAIChatClient (Responses API) shape. The previous implementation used async def without a stream parameter, which caused the framework's streaming path to receive a coroutine instead of an AsyncIterable, raising: 'async for' requires an object with __aiter__ method, got coroutine Mirror the existing AzureOpenAIResponseClientWithRetry pattern: sync _inner_get_response that branches on stream and delegates non-streaming calls to _non_streaming_with_retry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure_openai_response_retry.py | 136 +++++++++++++----- 1 file changed, 102 insertions(+), 34 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 9eff88e2..c3aafb75 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -691,35 +691,17 @@ def __init__( self._retry_config = retry_config or RateLimitRetryConfig.from_env() self._context_trim_config = ContextTrimConfig.from_env() - async def _inner_get_response( - self, - *, - messages: MutableSequence[Any], - options: Any | None = None, - **kwargs: Any, + def _inner_get_response( + self, *, messages: MutableSequence[Any], options: Any = None, stream: bool = False, **kwargs: Any ) -> Any: - """Override that adds retry + context-trimming around the parent call.""" - parent_inner_get_response = super( - AzureOpenAIChatClientWithRetry, self - )._inner_get_response + """Override that adds retry + context-trimming around the parent call. - effective_messages: MutableSequence[Any] | list[Any] = messages - if self._context_trim_config.enabled: - approx_chars = sum(len(_estimate_message_text(m)) for m in messages) - if ( - self._context_trim_config.max_total_chars > 0 - and approx_chars > self._context_trim_config.max_total_chars - ): - effective_messages = _trim_messages( - messages, cfg=self._context_trim_config - ) - logger.warning( - "[AOAI_CTX_TRIM] pre-trimmed chat request messages: approx_chars=%s -> %s; count=%s -> %s", - approx_chars, - sum(len(_estimate_message_text(m)) for m in effective_messages), - len(messages), - len(effective_messages), - ) + Must remain a regular ``def`` (not ``async def``) because the parent + returns different types depending on *stream*: + - stream=False → Awaitable[ChatResponse] + - stream=True → ResponseStream (AsyncIterable) + """ + effective_messages = self._maybe_trim_messages(messages) if not effective_messages: logger.warning( @@ -727,10 +709,68 @@ async def _inner_get_response( ) effective_messages = messages + if stream: + # For streaming, delegate to the parent which returns a proper + # ResponseStream. The framework checks isinstance(result, ResponseStream) + # and async generators fail that check. + parent_inner = super( + AzureOpenAIChatClientWithRetry, self + )._inner_get_response + return parent_inner( + messages=effective_messages, options=options, stream=True, **kwargs + ) + else: + return self._non_streaming_with_retry( + effective_messages=effective_messages, + original_messages=messages, + options=options, + **kwargs, + ) + + def _maybe_trim_messages( + self, messages: MutableSequence[Any] + ) -> MutableSequence[Any] | list[Any]: + """Apply pre-call context trimming if enabled and over budget.""" + if not self._context_trim_config.enabled: + return messages + approx_chars = sum(len(_estimate_message_text(m)) for m in messages) + if ( + self._context_trim_config.max_total_chars > 0 + and approx_chars > self._context_trim_config.max_total_chars + ): + trimmed = _trim_messages(messages, cfg=self._context_trim_config) + if not trimmed: + logger.warning( + "[AOAI_CTX_TRIM] trimming would remove all messages; keeping originals" + ) + return messages + logger.warning( + "[AOAI_CTX_TRIM] pre-trimmed chat request messages: approx_chars=%s -> %s; count=%s -> %s", + approx_chars, + sum(len(_estimate_message_text(m)) for m in trimmed), + len(messages), + len(trimmed), + ) + return trimmed + return messages + + async def _non_streaming_with_retry( + self, + *, + effective_messages: MutableSequence[Any] | list[Any], + original_messages: MutableSequence[Any], + options: Any = None, + **kwargs: Any, + ) -> Any: + """Non-streaming path: full retry + context-trim fallback.""" + parent_inner = super( + AzureOpenAIChatClientWithRetry, self + )._inner_get_response + try: return await _retry_call( - lambda: parent_inner_get_response( - messages=effective_messages, options=options, **kwargs + lambda: parent_inner( + messages=effective_messages, options=options, stream=False, **kwargs ), config=self._retry_config, ) @@ -742,20 +782,48 @@ async def _inner_get_response( ): raise - trimmed = _trim_messages(messages, cfg=self._context_trim_config) + trimmed = _trim_messages( + original_messages, + cfg=ContextTrimConfig( + enabled=True, + max_total_chars=max( + 50_000, self._context_trim_config.max_total_chars - 80_000 + ), + max_message_chars=max( + 3_000, self._context_trim_config.max_message_chars - 6_000 + ), + keep_last_messages=max( + 6, self._context_trim_config.keep_last_messages - 12 + ), + keep_head_chars=max( + 1_000, self._context_trim_config.keep_head_chars - 4_000 + ), + keep_tail_chars=self._context_trim_config.keep_tail_chars, + keep_system_messages=True, + retry_on_context_error=True, + ), + ) if not trimmed: logger.warning( - "[AOAI_CTX_TRIM] trim would remove all messages; re-raising original error" + "[AOAI_CTX_TRIM] aggressive trim would remove all messages; re-raising original error" ) raise logger.warning( "[AOAI_CTX_TRIM] retrying chat after context-length error; count=%s -> %s", - len(messages), + len(original_messages), len(trimmed), ) + trim_delay = min( + self._retry_config.base_delay_seconds, + self._retry_config.max_delay_seconds, + ) + logger.info( + "[AOAI_CTX_TRIM] sleeping %ss before retry", round(trim_delay, 1) + ) + await asyncio.sleep(trim_delay) return await _retry_call( - lambda: parent_inner_get_response( - messages=trimmed, options=options, **kwargs + lambda: parent_inner( + messages=trimmed, options=options, stream=False, **kwargs ), config=self._retry_config, ) From 2caced57a2e78813a53d2821921cf6cad4bb0c79 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 22:18:28 +0530 Subject: [PATCH 23/50] Sanitize message author_name for OpenAI Chat Completions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenAI's Chat Completions endpoint validates the message `name` field against the pattern `^[^\s<|\\/>]+$`. Our agents have display names with whitespace (e.g. `Chief Architect`, `AKS Expert`), which caused a 400 BadRequest after switching the default client to `AzureOpenAIChatClientWithRetry`. Add `_sanitize_author_name` / `_sanitize_author_names` helpers that replace runs of disallowed characters (whitespace, `<`, `|`, `\`, `/`, `>`) with a single underscore and strip leading/trailing underscores. Names that sanitize down to an empty string are dropped entirely so the field can be omitted from the request. The sanitizer is applied inside `AzureOpenAIChatClientWithRetry._inner_get_response` after context trimming (and again after the trim-fallback retry inside `_non_streaming_with_retry`) so the wire format passes validation while in-memory `Message` objects keep their original display names for orchestration logic. Originals are never mutated — modified messages are shallow-copied before the name is rewritten. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure_openai_response_retry.py | 88 +++++++++++++++++ .../test_azure_openai_response_retry_utils.py | 95 +++++++++++++++++++ 2 files changed, 183 insertions(+) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index c3aafb75..0ca89ff0 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -6,9 +6,11 @@ from __future__ import annotations import asyncio +import copy import logging import os import random +import re from dataclasses import dataclass from typing import Any, MutableSequence @@ -265,6 +267,85 @@ def _set_message_text(message: Any, new_text: str) -> Any: return message +# OpenAI Chat Completions requires message `name` to match this pattern: +# ^[^\s<|\\/>]+$ +# Agent display names like "Chief Architect" contain spaces and are rejected. +# We replace any run of disallowed characters with a single underscore so the +# wire-format passes validation while preserving readability. +_OPENAI_NAME_INVALID_CHARS = re.compile(r"[\s<|\\/>]+") + + +def _sanitize_author_name(name: Any) -> Any: + """Sanitize a single author_name for OpenAI Chat Completions. + + Returns the original value when it is not a string, is empty, or is already + valid. Otherwise returns a string with disallowed characters collapsed to + underscores and surrounding underscores stripped. If the result would be + empty (e.g. name was all whitespace), returns ``None`` so the field can be + dropped entirely. + """ + if not isinstance(name, str) or not name: + return name + if not _OPENAI_NAME_INVALID_CHARS.search(name): + return name + sanitized = _OPENAI_NAME_INVALID_CHARS.sub("_", name).strip("_") + return sanitized or None + + +def _sanitize_author_names( + messages: MutableSequence[Any], +) -> MutableSequence[Any] | list[Any]: + """Return ``messages`` with each entry's author_name sanitized. + + - For dict-shaped messages, the ``name`` key is rewritten on a shallow copy + (and removed if the sanitized value would be empty). + - For ``agent_framework.Message``-like objects, ``author_name`` is rewritten + on a shallow copy so the originals (which may live in long-lived agent + state) are not mutated. + - Messages that don't need sanitization are returned unchanged. If nothing + needed sanitization the original sequence is returned as-is. + """ + out: list[Any] = [] + any_changed = False + for m in messages: + # Dict form: {"role": ..., "name": ..., "content": ...} + if isinstance(m, dict): + name = m.get("name") + if isinstance(name, str): + sanitized = _sanitize_author_name(name) + if sanitized != name: + new_m = dict(m) + if sanitized: + new_m["name"] = sanitized + else: + new_m.pop("name", None) + out.append(new_m) + any_changed = True + continue + out.append(m) + continue + + # Object form (agent_framework Message): has .author_name attribute. + name = getattr(m, "author_name", None) + if isinstance(name, str): + sanitized = _sanitize_author_name(name) + if sanitized != name: + try: + new_m = copy.copy(m) + new_m.author_name = sanitized + out.append(new_m) + any_changed = True + continue + except Exception: + # Last-resort in-place fallback if copy/setattr is blocked. + try: + m.author_name = sanitized + except Exception: + pass + out.append(m) + return out if any_changed else messages + + @dataclass(frozen=True) class ContextTrimConfig: """Character-budget based context trimming. @@ -709,6 +790,11 @@ def _inner_get_response( ) effective_messages = messages + # OpenAI Chat Completions validates message `name` against ^[^\s<|\\/>]+$. + # Sanitize before sending so agent display names like "Chief Architect" + # don't trip a 400 BadRequest. Originals are shallow-copied, not mutated. + effective_messages = _sanitize_author_names(effective_messages) + if stream: # For streaming, delegate to the parent which returns a proper # ResponseStream. The framework checks isinstance(result, ResponseStream) @@ -813,6 +899,8 @@ async def _non_streaming_with_retry( len(original_messages), len(trimmed), ) + # Re-sanitize names on the freshly-trimmed messages before retry. + trimmed = _sanitize_author_names(trimmed) trim_delay = min( self._retry_config.base_delay_seconds, self._retry_config.max_delay_seconds, diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_azure_openai_response_retry_utils.py b/src/processor/src/tests/unit/libs/agent_framework/test_azure_openai_response_retry_utils.py index aba664fa..7236d192 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_azure_openai_response_retry_utils.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_azure_openai_response_retry_utils.py @@ -7,6 +7,8 @@ RateLimitRetryConfig, _looks_like_context_length, _looks_like_rate_limit, + _sanitize_author_name, + _sanitize_author_names, _trim_messages, _truncate_text, ) @@ -85,3 +87,96 @@ def test_trim_messages_keeps_system_and_tails_and_truncates_long_messages() -> N # The last message is intentionally never truncated (agent needs full context). assert len(trimmed[1]["content"]) <= 50 assert len(trimmed[2]["content"]) == 100 + + +# --------------------------------------------------------------------------- +# author_name sanitization (Chat Completions name pattern: ^[^\s<|\\/>]+$) +# --------------------------------------------------------------------------- + + +def test_sanitize_author_name_passthrough_for_valid_names() -> None: + assert _sanitize_author_name("Coordinator") == "Coordinator" + assert _sanitize_author_name("ResultGenerator") == "ResultGenerator" + assert _sanitize_author_name("agent-1_2.x") == "agent-1_2.x" + + +def test_sanitize_author_name_replaces_whitespace_and_specials() -> None: + assert _sanitize_author_name("Chief Architect") == "Chief_Architect" + assert _sanitize_author_name("AKS Expert") == "AKS_Expert" + # Tabs/newlines collapse to a single underscore. + assert _sanitize_author_name("a\tb\nc") == "a_b_c" + # Each disallowed char in the pattern is replaced. + assert _sanitize_author_name("foo/bar\\baz|quxy") == "foo_bar_baz_qux_x_y" + + +def test_sanitize_author_name_handles_edge_cases() -> None: + assert _sanitize_author_name(None) is None + assert _sanitize_author_name("") == "" + assert _sanitize_author_name(123) == 123 + # All-invalid input collapses to empty -> None (so callers drop the field). + assert _sanitize_author_name(" ") is None + # Leading/trailing underscores from sanitization are stripped. + assert _sanitize_author_name(" Chief Architect ") == "Chief_Architect" + + +def test_sanitize_author_names_dict_messages_shallow_copy() -> None: + original = [ + {"role": "system", "content": "sys"}, + {"role": "assistant", "name": "Chief Architect", "content": "hi"}, + {"role": "user", "name": "Coordinator", "content": "ok"}, + ] + out = _sanitize_author_names(original) + + # New list when changes happened. + assert out is not original + # Originals untouched. + assert original[1]["name"] == "Chief Architect" + # Unchanged messages share identity with originals (shallow copy only when needed). + assert out[0] is original[0] + assert out[2] is original[2] + # Changed message is a new dict with sanitized name. + assert out[1] is not original[1] + assert out[1]["name"] == "Chief_Architect" + assert out[1]["content"] == "hi" + + +def test_sanitize_author_names_dict_messages_drops_empty_name() -> None: + original = [ + {"role": "assistant", "name": " ", "content": "hello"}, + ] + out = _sanitize_author_names(original) + assert "name" not in out[0] + assert out[0]["content"] == "hello" + + +def test_sanitize_author_names_returns_input_when_nothing_changes() -> None: + original = [ + {"role": "system", "content": "sys"}, + {"role": "assistant", "name": "Coordinator", "content": "hi"}, + ] + out = _sanitize_author_names(original) + # Same sequence object returned to avoid pointless copies. + assert out is original + + +def test_sanitize_author_names_object_messages_shallow_copy() -> None: + class _Msg: + def __init__(self, role: str, author_name: str | None, content: str) -> None: + self.role = role + self.author_name = author_name + self.content = content + + m1 = _Msg("assistant", "Chief Architect", "hi") + m2 = _Msg("assistant", "Coordinator", "ok") + original = [m1, m2] + + out = _sanitize_author_names(original) + + # Original object untouched. + assert m1.author_name == "Chief Architect" + # Changed message replaced with a shallow copy carrying sanitized name. + assert out[0] is not m1 + assert out[0].author_name == "Chief_Architect" + assert out[0].content == "hi" + # Unchanged message is the same instance. + assert out[1] is m2 From 7f8a04b62623458d78c191ff8a9ec9fa45610983 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 22:41:55 +0530 Subject: [PATCH 24/50] Demote empty-messages warning to debug The [AOAI_RETRY] empty messages list received warning fired on every turn in group-chat orchestration when the same speaker was selected twice in a row, flooding logs and giving the false impression of an error. This pattern is by design in agent-framework's GroupChatOrchestrator: _broadcast_messages_to_participants excludes the source executor, so when the orchestrator routes back to the same agent, its message cache is empty. The framework already emits its own "AgentExecutor ... Running agent with empty message cache" warning for this case. The actual API call is not empty -- the parent OpenAIChatCompletionClient._prepare_options prepends the agent's system instructions from options["instructions"] before sending. So demoting our duplicate warning to DEBUG removes the noise without hiding any real failure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure_openai_response_retry.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 0ca89ff0..21bf2005 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -625,8 +625,12 @@ def _inner_get_response( effective_messages = self._maybe_trim_messages(messages) if not effective_messages: - logger.warning( - "[AOAI_RETRY] empty messages list received; using original messages" + # Empty inputs occur legitimately in group-chat orchestration when the + # same speaker is selected twice in a row (the orchestrator's broadcast + # excludes the source). The parent client's `_prepare_options` still + # prepends the agent's system instructions, so the API call has content. + logger.debug( + "[AOAI_RETRY] empty messages list received; relying on options.instructions" ) effective_messages = messages @@ -785,8 +789,12 @@ def _inner_get_response( effective_messages = self._maybe_trim_messages(messages) if not effective_messages: - logger.warning( - "[AOAI_RETRY] empty messages list received; using original messages" + # Empty inputs occur legitimately in group-chat orchestration when the + # same speaker is selected twice in a row (the orchestrator's broadcast + # excludes the source). The parent client's `_prepare_options` still + # prepends the agent's system instructions, so the API call has content. + logger.debug( + "[AOAI_RETRY] empty messages list received; relying on options.instructions" ) effective_messages = messages From 6d371fd42e3ac1e11fdc1b94e783dcfc2d9360cb Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 22:55:51 +0530 Subject: [PATCH 25/50] Fix Coordinator anti-loop: accept framework's response schema The agent_framework_orchestrations.GroupChatBuilder forces the Coordinator's response_format to AgentOrchestrationOutput (strict schema with fields next_speaker/reason/terminate). Our prompt asks for selected_participant/instruction/finish, but strict structured output overrides the prompt's field names. Without aliases, ManagerSelectionResponse.model_validate() silently succeeded with all fields = None (extra=allow), which disabled: - The 3-strike loop-detection streak counter (line 1019-1054) - Coordinator-driven termination on finish=true (line 1065) - _agent_invoked_at[selected] elapsed-time tracking (line 1098) Use Pydantic AliasChoices so the model accepts BOTH naming conventions, restoring anti-loop and termination logic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 5c3fc9ab..856575e9 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -33,7 +33,7 @@ ) from agent_framework.orchestrations import GroupChatBuilder from mem0 import AsyncMemory -from pydantic import BaseModel, ValidationError +from pydantic import AliasChoices, BaseModel, Field, ValidationError logger = logging.getLogger(__name__) @@ -44,14 +44,37 @@ class ManagerSelectionResponse(BaseModel): - """Coordinator selection payload parsed from JSON output.""" + """Coordinator selection payload parsed from JSON output. + + The Coordinator prompt instructs the model to emit fields named + ``selected_participant`` / ``instruction`` / ``finish``. However, the + underlying ``agent_framework_orchestrations.GroupChatBuilder`` forces the + Coordinator's response_format to ``AgentOrchestrationOutput`` (strict + schema with fields ``next_speaker`` / ``reason`` / ``terminate``). With + strict structured output, the model always emits the framework's field + names regardless of the prompt. + + We use Pydantic ``AliasChoices`` so this model accepts BOTH naming + conventions transparently. Without these aliases, parsing silently + succeeds (``extra=allow``) but every field ends up ``None``, disabling + loop detection and Coordinator-driven termination. + """ - selected_participant: str | None = None - instruction: str | None = None - finish: bool | None = None + selected_participant: str | None = Field( + default=None, + validation_alias=AliasChoices("selected_participant", "next_speaker"), + ) + instruction: str | None = Field( + default=None, + validation_alias=AliasChoices("instruction", "reason"), + ) + finish: bool | None = Field( + default=None, + validation_alias=AliasChoices("finish", "terminate"), + ) final_message: str | None = None - model_config = {"extra": "allow"} + model_config = {"extra": "allow", "populate_by_name": True} @dataclass From d5da788b0e17c38b8d7dda3b86c9b8637749827f Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 23:19:10 +0530 Subject: [PATCH 26/50] Sanitize message name at OpenAI wire format (defense in depth) Production still hits 400 BadRequest on messages[N].name even though _inner_get_response runs _sanitize_author_names on incoming Messages. The framework's _prepare_options/_prepare_messages_for_openai layer or agent-internal compaction can materialize messages with author_name set AFTER our early sanitization, leaving the dict 'name' field unsanitized on the wire. Override _prepare_messages_for_openai (the parent method that builds the final OpenAI dict payload) to sanitize each dict's 'name' field as a last-mile pass. This is the single chokepoint guaranteed to be on every Chat Completions request, regardless of upstream message-construction path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../azure_openai_response_retry.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index 21bf2005..b597afac 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -801,6 +801,12 @@ def _inner_get_response( # OpenAI Chat Completions validates message `name` against ^[^\s<|\\/>]+$. # Sanitize before sending so agent display names like "Chief Architect" # don't trip a 400 BadRequest. Originals are shallow-copied, not mutated. + # NOTE: this is a defense-in-depth pass on ``Message.author_name``. + # The authoritative sanitization happens in ``_prepare_messages_for_openai`` + # below, which sanitizes the FINAL dict ``name`` field right before the + # request is sent — catching any name that slips in via framework-internal + # message construction (e.g. compaction, memory context providers, + # orchestrator-injected messages) that bypasses this early pass. effective_messages = _sanitize_author_names(effective_messages) if stream: @@ -821,6 +827,47 @@ def _inner_get_response( **kwargs, ) + def _prepare_messages_for_openai(self, chat_messages, *args: Any, **kwargs: Any): # type: ignore[override] + """Sanitize message ``name`` fields after framework conversion to wire format. + + The parent ``_prepare_messages_for_openai`` walks ``Message`` objects and + builds the OpenAI dict payload (``{"role": ..., "name": ..., "content": ...}``). + The ``name`` field is copied from ``Message.author_name`` and is validated + by the OpenAI Chat Completions API against ``^[^\\s<|\\\\/>]+$``. + + We override here as a final, authoritative sanitization point. Even though + ``_inner_get_response`` already sanitizes ``Message.author_name``, names + can still reach this layer unsanitized from: + + * ``OpenAIChatCompletionClient._prepare_options`` calling + ``prepend_instructions_to_messages`` (which does not author_name, but + downstream callers may add named messages). + * ``ChatAgent`` / memory context providers materializing messages with + ``author_name`` set inside the agent run loop, after the client receives + the original sequence. + * Any framework-internal compaction or message-rewriting path that + constructs new ``Message`` objects. + + Sanitizing the dict output is the single chokepoint guaranteed to be + on every Chat Completions request, regardless of how the messages were + assembled upstream. + """ + result = super()._prepare_messages_for_openai(chat_messages, *args, **kwargs) + for msg in result: + if not isinstance(msg, dict): + continue + name = msg.get("name") + if not isinstance(name, str): + continue + sanitized = _sanitize_author_name(name) + if sanitized == name: + continue + if sanitized: + msg["name"] = sanitized + else: + msg.pop("name", None) + return result + def _maybe_trim_messages( self, messages: MutableSequence[Any] ) -> MutableSequence[Any] | list[Any]: From 0da531fdfd92d9c9afe42b0670e9fe81b51bc543 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 23:42:04 +0530 Subject: [PATCH 27/50] Fix loop detection: don't count looped-on agent's runs as progress When Coordinator keeps picking the same agent A and A keeps running, A's own completions were bumping _progress_counter. Loop detection compares the counter snapshot taken at the previous identical Coordinator pick against the current value; if it changed, the streak was reset to 1. So the 3-strike threshold was never reached and the Coordinator->A->A pattern ran until max_rounds. Now we only treat a non-Coordinator completion as 'progress' when the completing agent is different from the agent the Coordinator is currently latching onto (_last_coordinator_selection[0]). A different agent stepping in still resets the streak; A repeating itself does not. Adds two regression tests covering both cases. Also updates an existing termination test whose name described 'other agent makes progress' but actually used the same agent, hard-coding the buggy semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 21 ++++- .../test_groupchat_orchestrator_internals.py | 86 +++++++++++++++++++ ...test_groupchat_orchestrator_termination.py | 4 +- 3 files changed, 108 insertions(+), 3 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 856575e9..5d847554 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -1018,8 +1018,27 @@ async def _complete_agent_response( # Mark progress on any non-Coordinator completion. This is used to ensure loop # detection only triggers when the Coordinator is repeating itself *and* the # rest of the conversation is not advancing. + # + # IMPORTANT: we must NOT count the looped-on agent's own runs as "progress". + # If we did, then the pattern "Coordinator picks A -> A runs -> Coordinator + # picks A -> A runs -> ..." would keep bumping the progress counter, which + # would reset the loop-detection streak on every check, and the streak would + # never grow past 1. The loop would then never be detected. + # + # Real progress means a DIFFERENT agent ran since the last identical Coordinator + # selection. So we only increment when the completing agent is not the one the + # Coordinator is currently latching onto. if agent_name != self.coordinator_name: - self._progress_counter += 1 + last_selected = ( + self._last_coordinator_selection[0] + if self._last_coordinator_selection + else None + ) + if ( + last_selected is None + or agent_name.lower() != last_selected.lower() + ): + self._progress_counter += 1 # Detect manager termination signal (finish=true) from Coordinator. # NOTE: The underlying GroupChatBuilder does not automatically stop on finish, diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index 600869a0..27b7ac41 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -700,6 +700,92 @@ def _select(participant: str, instruction: str = "do"): assert orch._forced_termination_requested is True + def test_loop_breaker_triggered_when_looped_agent_runs_between_selections( + self, + ): + """Regression: when Coordinator keeps picking the same agent, that agent's + own runs MUST NOT count as progress, or the streak resets and the loop + never breaks. + """ + orch = _make_orch() + orch._conversation = [] + + def _select(participant: str, instruction: str = "do"): + orch._current_agent_response = [ + json.dumps( + { + "selected_participant": participant, + "instruction": instruction, + "finish": False, + "final_message": "", + } + ) + ] + orch._current_agent_start_time = datetime.now() + + def _agent_runs(name: str, text: str = "ok"): + orch._current_agent_response = [text] + orch._current_agent_start_time = datetime.now() + + # Simulate production sequence: Coordinator picks A, then A runs, + # then Coordinator picks A again, then A runs, etc. + _select("A") + _run(orch._complete_agent_response("Coordinator", None)) + _agent_runs("A") + _run(orch._complete_agent_response("A", None)) + _select("A") + _run(orch._complete_agent_response("Coordinator", None)) + _agent_runs("A") + _run(orch._complete_agent_response("A", None)) + _select("A") + _run(orch._complete_agent_response("Coordinator", None)) + + assert orch._forced_termination_requested is True + + def test_loop_breaker_resets_when_different_agent_responds(self): + """If a different agent responds between identical Coordinator selections, + treat that as real progress and reset the streak. + """ + orch = _make_orch() + orch._conversation = [] + + def _select(participant: str, instruction: str = "do"): + orch._current_agent_response = [ + json.dumps( + { + "selected_participant": participant, + "instruction": instruction, + "finish": False, + "final_message": "", + } + ) + ] + orch._current_agent_start_time = datetime.now() + + def _agent_runs(name: str, text: str = "ok"): + orch._current_agent_response = [text] + orch._current_agent_start_time = datetime.now() + + # Sequence: A, A, B, A, A (a different agent B interrupts -> streak resets) + _select("A") + _run(orch._complete_agent_response("Coordinator", None)) + _agent_runs("A") + _run(orch._complete_agent_response("A", None)) + _select("A") + _run(orch._complete_agent_response("Coordinator", None)) + _agent_runs("B") + _run(orch._complete_agent_response("B", None)) + _select("A") + _run(orch._complete_agent_response("Coordinator", None)) + _agent_runs("A") + _run(orch._complete_agent_response("A", None)) + _select("A") + _run(orch._complete_agent_response("Coordinator", None)) + + # Only 2 consecutive A selections without progress (one streak of 2 + # before B reset it, one streak of 2 after). Loop NOT detected. + assert orch._forced_termination_requested is False + # ----------------------------------------------------------------------------- # _build_groupchat diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py index dc7f124d..36a0c00b 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py @@ -113,9 +113,9 @@ def _agent_reply(text: str = "ok"): _coordinator_select("Chief Architect") await orch._complete_agent_response("Coordinator", callback=None) - # 2) The participant responds (progress). + # 2) A DIFFERENT participant responds (real progress, not the looped-on one). _agent_reply("progress") - await orch._complete_agent_response("Chief Architect", callback=None) + await orch._complete_agent_response("AKS Expert", callback=None) # 3) Coordinator repeats the same selection twice. _coordinator_select("Chief Architect") From 9a85f004c374eecd7d7c37f8e0f1ca6eeb25dbe9 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Fri, 12 Jun 2026 23:50:16 +0530 Subject: [PATCH 28/50] Add agent_description for Analysis participants so Coordinator can route by capability The Coordinator's valid_participants block was a bullet list of names only, so the LLM had no per-agent capability signal. Combined with a Coordinator prompt that names 'Chief Architect' frequently across phases 0/1/4/5/6, the model latched onto Chief Architect repeatedly and the conversation looped on the same agent. This change populates agent_description on every Analysis participant (Chief Architect, AKS Expert, and the platform experts in platform_registry.json) and renders each description into the Coordinator's valid_participants list. The descriptions are also passed through AgentBuilder.create_agent_by_agentinfo's existing description= argument, so the framework's Agent.description field is no longer always None. Scope: Analysis step only. design/yaml/documentation orchestrators are left for a follow-up after this change is validated in production. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../orchestration/analysis_orchestrator.py | 30 +++++++++++++++++-- .../orchestration/platform_registry.json | 18 +++++++---- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py b/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py index 1221433f..4705d240 100644 --- a/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py +++ b/src/processor/src/steps/analysis/orchestration/analysis_orchestrator.py @@ -157,8 +157,13 @@ async def prepare_agent_infos(self) -> list[AgentInfo]: prompt_path = agent_dir / "agents" / prompt_file instruction = self.read_prompt_file(str(prompt_path)) + expert_description = expert.get("description") + if not isinstance(expert_description, str) or not expert_description.strip(): + expert_description = None + expert_info = AgentInfo( agent_name=agent_name, + agent_description=expert_description, agent_instruction=instruction, tools=self.mcp_tools, ) @@ -172,6 +177,12 @@ async def prepare_agent_infos(self) -> list[AgentInfo]: aks_instruction = self.read_prompt_file(agent_dir / "agents/prompt_aks.txt") aks_agent_info = AgentInfo( agent_name="AKS Expert", + agent_description=( + "Tool-free reviewer for Azure AKS migration readiness. Reviews the " + "latest Evidence Pack and produces SIGN-OFF: PASS/FAIL on AKS-side " + "concerns. Select only after the Chief Architect has posted an Evidence " + "Pack; required reviewer when present." + ), agent_instruction=aks_instruction, tools=self.mcp_tools, ) @@ -188,6 +199,13 @@ async def prepare_agent_infos(self) -> list[AgentInfo]: chief_architect_agent_info = AgentInfo( agent_name="Chief Architect", + agent_description=( + "Lead orchestrator of the analysis and the ONLY participant with blob " + "tools. Performs all file I/O (listing, reading, writing " + "analysis_result.md) and authors / refreshes the Evidence Pack. Select " + "to run hard-termination triage, to do or redo any file work, or to " + "post a fresh Evidence Pack after reviewers ask for one." + ), agent_instruction=architect_instruction, tools=self.mcp_tools, ) @@ -208,10 +226,18 @@ async def prepare_agent_infos(self) -> list[AgentInfo]: tools=self.mcp_tools[2], # Blob IO tool only ) - # Render coordinator prompt with the current participant list. + # Render coordinator prompt with the current participant list. Include each + # participant's description so the Coordinator can route by capability rather + # than only by name (a name-only list biases the LLM toward whichever name + # appears most often elsewhere in the prompt, e.g. "Chief Architect"). participant_names = [ai.agent_name for ai in agent_infos] valid_participants_block = "\n".join([ - f'- "{name}"' for name in participant_names + ( + f'- "{ai.agent_name}": {ai.agent_description}' + if ai.agent_description + else f'- "{ai.agent_name}"' + ) + for ai in agent_infos ]) coordinator_agent_info.render( **self.task_param.model_dump(), diff --git a/src/processor/src/steps/analysis/orchestration/platform_registry.json b/src/processor/src/steps/analysis/orchestration/platform_registry.json index 342f10b6..a0342846 100644 --- a/src/processor/src/steps/analysis/orchestration/platform_registry.json +++ b/src/processor/src/steps/analysis/orchestration/platform_registry.json @@ -6,7 +6,8 @@ "agent_name": "EKS Expert", "prompt_file": "prompt_eks.txt", "category": "managed_cloud", - "signals": ["eks.amazonaws.com", "aws", "arn:aws", "kops", "eksctl"] + "signals": ["eks.amazonaws.com", "aws", "arn:aws", "kops", "eksctl"], + "description": "Tool-free reviewer specialized in Amazon EKS (AWS) source-side concerns. Provides EKS-specific analysis of the Evidence Pack and produces SIGN-OFF: PASS/FAIL. Select only after the Chief Architect has posted an Evidence Pack; required only if the source platform is EKS." }, { "id": "gke", @@ -19,7 +20,8 @@ "anthos", "gcr.io", "artifactregistry" - ] + ], + "description": "Tool-free reviewer specialized in Google GKE source-side concerns. Provides GKE-specific analysis of the Evidence Pack and produces SIGN-OFF: PASS/FAIL. Select only after the Chief Architect has posted an Evidence Pack; required only if the source platform is GKE." }, { "id": "openshift", @@ -32,21 +34,24 @@ "ocp", "security.openshift.io", "config.openshift.io" - ] + ], + "description": "Tool-free reviewer for Red Hat OpenShift (OCP) source-side concerns. Reviews the Evidence Pack and produces SIGN-OFF: PASS/FAIL. Select only after the Chief Architect has posted an Evidence Pack; required only if the source platform is OpenShift." }, { "id": "rancher", "agent_name": "Rancher Expert", "prompt_file": "prompt_rancher.txt", "category": "enterprise_distro", - "signals": ["cattle.io", "rancher", "rke", "rke2", "k3s"] + "signals": ["cattle.io", "rancher", "rke", "rke2", "k3s"], + "description": "Tool-free reviewer for Rancher / RKE / RKE2 / K3s source-side concerns. Reviews the Evidence Pack and produces SIGN-OFF: PASS/FAIL. Select only after the Chief Architect has posted an Evidence Pack; required only if the source platform is Rancher." }, { "id": "tanzu", "agent_name": "Tanzu Expert", "prompt_file": "prompt_tanzu.txt", "category": "enterprise_distro", - "signals": ["tanzu", "vmware", "tkg", "tkgs", "pinniped", "antrea"] + "signals": ["tanzu", "vmware", "tkg", "tkgs", "pinniped", "antrea"], + "description": "Tool-free reviewer for VMware Tanzu (TKG / TKGS) source-side concerns. Reviews the Evidence Pack and produces SIGN-OFF: PASS/FAIL. Select only after the Chief Architect has posted an Evidence Pack; required only if the source platform is Tanzu." }, { "id": "onprem", @@ -70,7 +75,8 @@ "ceph", "nfs", "f5" - ] + ], + "description": "Tool-free reviewer for self-managed / on-premises Kubernetes (kubeadm, k3s, Talos, bare-metal, vSphere) source-side concerns. Reviews the Evidence Pack and produces SIGN-OFF: PASS/FAIL. Select only after the Chief Architect has posted an Evidence Pack; required only if the source platform is on-prem." } ] } From 76e9c17c86c124cd4bfddf83bf7deae9ed1a5fef Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Sat, 13 Jun 2026 00:05:45 +0530 Subject: [PATCH 29/50] Fix loop detection: key on agent name only, not (agent, instruction) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production run after the previous progress-counter fix (0da531f) STILL showed Chief Architect picked 6+ consecutive times. Root cause: the loop detection key was (agent, instruction_text). The LLM-driven Coordinator varies its instruction on every pick ('list source blobs', 'read xyz.yaml', 'save analysis_result.md') while latching onto the same agent — so every selection_key was unique, the streak reset to 1 on every pick, and the 3-strike threshold was never reached. Change: track only the agent name (lower-cased). The progress counter (now correct after 0da531f) already encodes 'no DIFFERENT agent ran in between', so 3 consecutive picks of the same agent with no other-agent progress is a strong, low-false-positive loop signal. Adds a regression test that replays the production sequence (same agent, three different instruction strings) and verifies forced termination fires. The earlier tests for exact-match repeats and for B-resets-the- streak continue to pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 50 ++++++++++++------- .../test_groupchat_orchestrator_internals.py | 47 +++++++++++++++++ 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 5d847554..9d833684 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -295,9 +295,16 @@ def __init__( self._forced_termination_reason: str | None = None self._forced_termination_type: str | None = None - # Loop detection for Coordinator selections (participant + instruction) - self._last_coordinator_selection: tuple[str, str] | None = None + # Loop detection for Coordinator selections. + # We track the *agent the Coordinator most recently picked* (lower-cased name) + # rather than (agent, instruction) tuples, because in practice the LLM-driven + # Coordinator varies the instruction text while looping on the same agent. + # A streak counts how many consecutive Coordinator picks landed on the same + # agent without any *other* agent running in between (see _progress_counter + # bookkeeping in _handle_agent_update). + self._last_coordinator_selection: str | None = None self._coordinator_selection_streak: int = 0 + # Diagnostic history of recent (agent, instruction) selections. self._recent_coordinator_selections: deque[tuple[str, str]] = deque(maxlen=10) # Progress counter used to avoid false-positive loop detection. @@ -1029,14 +1036,10 @@ async def _complete_agent_response( # selection. So we only increment when the completing agent is not the one the # Coordinator is currently latching onto. if agent_name != self.coordinator_name: - last_selected = ( - self._last_coordinator_selection[0] - if self._last_coordinator_selection - else None - ) + last_selected = self._last_coordinator_selection if ( last_selected is None - or agent_name.lower() != last_selected.lower() + or agent_name.lower() != last_selected ): self._progress_counter += 1 @@ -1058,17 +1061,27 @@ async def _complete_agent_response( # measures from Coordinator selection -> response completion. selected = getattr(manager_response, "selected_participant", None) - # Loop detection: same selection+instruction repeated. + # Loop detection: same agent picked repeatedly with no other agent + # making progress in between. We deliberately key on the agent name + # alone (not on the instruction text) because the LLM-driven + # Coordinator often varies its instruction text while still looping + # on the same agent ("re-list", "read xyz.yaml", "save analysis_result.md" + # all sent to the same Chief Architect over and over). The + # _progress_counter (incremented in _handle_agent_update only when + # a DIFFERENT agent runs) is what tells us whether anything else + # actually happened in between. if ( isinstance(selected, str) and selected and selected.lower() != "none" ): - selection_key = (selected, str(manager_instruction or "")) - self._recent_coordinator_selections.append(selection_key) - if selection_key == self._last_coordinator_selection: - # If any other agent responded since the last identical selection, - # treat that as progress and reset the streak. + selected_key = selected.lower() + self._recent_coordinator_selections.append( + (selected, str(manager_instruction or "")) + ) + if selected_key == self._last_coordinator_selection: + # Same agent again. If any other agent ran since the last + # identical pick, treat that as progress and reset the streak. if ( self._progress_counter != self._last_coordinator_selection_progress @@ -1080,17 +1093,20 @@ async def _complete_agent_response( else: self._coordinator_selection_streak += 1 else: - self._last_coordinator_selection = selection_key + self._last_coordinator_selection = selected_key self._coordinator_selection_streak = 1 self._last_coordinator_selection_progress = ( self._progress_counter ) - # If the Coordinator repeats the exact same ask 3 times, break. + # If the Coordinator picks the same agent 3 times in a row + # without any other agent running in between, break out. if self._coordinator_selection_streak >= 3: self._request_forced_termination( reason=( - f"Loop detected: Coordinator repeated the same selection to '{selected}' {self._coordinator_selection_streak} times with no progress" + f"Loop detected: Coordinator selected '{selected}' " + f"{self._coordinator_selection_streak} consecutive " + f"times with no other agent making progress in between" ), termination_type="hard_timeout", ) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index 27b7ac41..5a1121c8 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -786,6 +786,53 @@ def _agent_runs(name: str, text: str = "ok"): # before B reset it, one streak of 2 after). Loop NOT detected. assert orch._forced_termination_requested is False + def test_loop_breaker_triggered_when_same_agent_picked_with_varying_instructions( + self, + ): + """Regression for production: the LLM-driven Coordinator was looping on + Chief Architect but varying its instruction text on every pick + ('re-list', 'read xyz', 'save analysis_result.md'). The loop detector + must key on the AGENT NAME only — not on (agent, instruction) — or the + streak resets on every pick and the loop is never caught. + """ + orch = _make_orch() + orch._conversation = [] + + def _select(participant: str, instruction: str = "do"): + orch._current_agent_response = [ + json.dumps( + { + "selected_participant": participant, + "instruction": instruction, + "finish": False, + "final_message": "", + } + ) + ] + orch._current_agent_start_time = datetime.now() + + def _agent_runs(name: str, text: str = "ok"): + orch._current_agent_response = [text] + orch._current_agent_start_time = datetime.now() + + # Each Coordinator pick targets the same agent but with a DIFFERENT + # instruction. With the old (agent, instruction) tuple key this never + # tripped the breaker. + _select("Chief Architect", instruction="list source blobs") + _run(orch._complete_agent_response("Coordinator", None)) + _agent_runs("Chief Architect") + _run(orch._complete_agent_response("Chief Architect", None)) + + _select("Chief Architect", instruction="read source files") + _run(orch._complete_agent_response("Coordinator", None)) + _agent_runs("Chief Architect") + _run(orch._complete_agent_response("Chief Architect", None)) + + _select("Chief Architect", instruction="save analysis_result.md") + _run(orch._complete_agent_response("Coordinator", None)) + + assert orch._forced_termination_requested is True + # ----------------------------------------------------------------------------- # _build_groupchat From 4de1e284845e9a82e7ecbead9400cd9d277a40b9 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Sat, 13 Jun 2026 00:45:04 +0530 Subject: [PATCH 30/50] fix(workflow): reject incoherent ResultGenerator output across all steps Production deployment of the agent-framework 1.3.0 upgrade surfaced a crash chain: Analysis "succeeded" with a self-contradictory result (result=True, is_hard_terminated=False, output=None), Design then crashed at `task_param.output.process_id`. The root cause is the ResultGenerator returning an empty shell when participants never produced useful content. Fixes: * groupchat_orchestrator.run_stream now validates ResultGenerator output before constructing OrchestrationResult. If the result is not hard terminated but carries no `output` / `termination_output` payload, the orchestrator now reports success=False with a descriptive error. This is generic across all four step models (Analysis uses `output`; Design/Convert/Documentation use `termination_output`). * All four step executors gained a defense-in-depth guard that raises a clear `Executor failed: produced no Output. Reason: ...` exception when the same incoherent shape is observed. This stops the broken value at the boundary instead of propagating it downstream. * groupchat_orchestrator silent `except Exception: pass` around Coordinator JSON parsing replaced with `logger.debug(... exc_info=...)` so loop-detection failures become visible during debugging instead of being swallowed. Tests: * Updated each executor's existing soft-completion test to provide a valid output (previous setup encoded the broken shape we now reject). * Added a new guard test per executor asserting the new exception fires for the incoherent (success=True + output=None + not hard-terminated) shape. * Full unit suite: 829 passed (was 825; +4 new guard tests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 55 ++++++++++- .../analysis/workflow/analysis_executor.py | 7 ++ .../convert/workflow/yaml_convert_executor.py | 7 ++ .../steps/design/workflow/design_executor.py | 7 ++ .../workflow/documentation_executor.py | 7 ++ .../steps/analysis/test_analysis_executor.py | 99 ++++++++++++++++++- .../convert/test_yaml_convert_executor.py | 99 ++++++++++++++++++- .../unit/steps/design/test_design_executor.py | 61 +++++++++++- .../test_documentation_executor.py | 76 +++++++++++++- 9 files changed, 409 insertions(+), 9 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 9d833684..05034ddd 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -643,17 +643,48 @@ async def run_stream( f"[RESULT] Skipping result generation - result_format: {result_format}, agent exists: {result_generator_name in self.agents}" ) + # Validate that ResultGenerator produced a coherent output. The LLM can + # sometimes return is_hard_terminated=False with output=None ("success + # but no actual output"), which causes downstream steps to crash with + # NoneType errors. Treat such self-contradictory results as failures so + # the workflow surfaces a clear error rather than propagating an empty + # shell to the next step. + generated_error: str | None = None + if final_analysis is not None and not bool( + getattr(final_analysis, "is_hard_terminated", False) + ): + # Step result models use either ``output`` (Analysis) or + # ``termination_output`` (Design, Convert, Documentation). Treat + # both equivalently: if neither holds a non-None payload, the + # ResultGenerator returned an incoherent shell. + has_output_attr = hasattr(final_analysis, "output") or hasattr( + final_analysis, "termination_output" + ) + payload = getattr(final_analysis, "output", None) or getattr( + final_analysis, "termination_output", None + ) + if has_output_attr and payload is None: + reason = ( + getattr(final_analysis, "reason", "") or "" + ) + generated_error = ( + "ResultGenerator produced incoherent output: " + "is_hard_terminated=False but output=None. " + f"Reason from result: {reason}" + ) + logger.error("[RESULT] %s", generated_error) + # Calculate execution time execution_time = (datetime.now() - start_time).total_seconds() # Build result result = OrchestrationResult[TOutput]( - success=True, + success=generated_error is None, conversation=conversation, agent_responses=self.agent_responses, tool_usage=self.agent_tool_usage, result=final_analysis, - error=None, + error=generated_error, execution_time_seconds=execution_time, ) @@ -1154,9 +1185,23 @@ async def _complete_agent_response( ): # Record invocation time for non-termination coordinator selections self._agent_invoked_at[selected] = completed_at - except Exception: - # If the Coordinator didn't emit valid JSON, ignore. - pass + except Exception as exc: + # If the Coordinator didn't emit valid JSON we silently drop + # loop-detection and termination handling for this turn. Log at + # debug so the silence is visible if loop detection ever appears + # to misfire (previously this was a bare ``pass`` which made the + # failure invisible). + preview = ( + complete_message[:200] + if isinstance(complete_message, str) + else str(type(complete_message)) + ) + logger.debug( + "Coordinator JSON parse failed; skipping loop detection for " + "this turn. Raw message preview: %r", + preview, + exc_info=exc, + ) # Invoke callback with complete response if callback: diff --git a/src/processor/src/steps/analysis/workflow/analysis_executor.py b/src/processor/src/steps/analysis/workflow/analysis_executor.py index 924a9289..999ae725 100644 --- a/src/processor/src/steps/analysis/workflow/analysis_executor.py +++ b/src/processor/src/steps/analysis/workflow/analysis_executor.py @@ -65,6 +65,13 @@ async def handle_execute( error_msg = result.error or "Analysis orchestration failed with no output" raise Exception(f"AnalysisExecutor failed: {error_msg}") + if not result.result.is_hard_terminated and result.result.output is None: + reason = result.result.reason or "" + raise Exception( + "AnalysisExecutor failed: orchestration reported success but produced " + f"no AnalysisOutput. Reason: {reason}" + ) + if result.result: if not result.result.is_hard_terminated: await ctx.send_message(result.result) diff --git a/src/processor/src/steps/convert/workflow/yaml_convert_executor.py b/src/processor/src/steps/convert/workflow/yaml_convert_executor.py index 7a9e283f..195de773 100644 --- a/src/processor/src/steps/convert/workflow/yaml_convert_executor.py +++ b/src/processor/src/steps/convert/workflow/yaml_convert_executor.py @@ -45,6 +45,13 @@ async def handle_execute( ) raise Exception(f"YamlConvertExecutor failed: {error_msg}") + if not result.result.is_hard_terminated and result.result.termination_output is None: + reason = result.result.reason or "" + raise Exception( + "YamlConvertExecutor failed: orchestration reported success but " + f"produced no YAML conversion output. Reason: {reason}" + ) + if result.result: if not result.result.is_hard_terminated: await ctx.send_message(result.result) diff --git a/src/processor/src/steps/design/workflow/design_executor.py b/src/processor/src/steps/design/workflow/design_executor.py index 986fed12..b70ed75c 100644 --- a/src/processor/src/steps/design/workflow/design_executor.py +++ b/src/processor/src/steps/design/workflow/design_executor.py @@ -42,6 +42,13 @@ async def handle_execute( error_msg = result.error or "Design orchestration failed with no output" raise Exception(f"DesignExecutor failed: {error_msg}") + if not result.result.is_hard_terminated and result.result.termination_output is None: + reason = result.result.reason or "" + raise Exception( + "DesignExecutor failed: orchestration reported success but produced " + f"no DesignOutput. Reason: {reason}" + ) + if result.result: if not result.result.is_hard_terminated: await ctx.send_message(result.result) diff --git a/src/processor/src/steps/documentation/workflow/documentation_executor.py b/src/processor/src/steps/documentation/workflow/documentation_executor.py index 9347b13f..64070201 100644 --- a/src/processor/src/steps/documentation/workflow/documentation_executor.py +++ b/src/processor/src/steps/documentation/workflow/documentation_executor.py @@ -47,4 +47,11 @@ async def handle_execute( ) raise Exception(f"DocumentationExecutor failed: {error_msg}") + if not result.result.is_hard_terminated and result.result.termination_output is None: + reason = result.result.reason or "" + raise Exception( + "DocumentationExecutor failed: orchestration reported success but " + f"produced no DocumentationOutput. Reason: {reason}" + ) + await ctx.yield_output(result.result) diff --git a/src/processor/src/tests/unit/steps/analysis/test_analysis_executor.py b/src/processor/src/tests/unit/steps/analysis/test_analysis_executor.py index 1c2d1298..c3d7613f 100644 --- a/src/processor/src/tests/unit/steps/analysis/test_analysis_executor.py +++ b/src/processor/src/tests/unit/steps/analysis/test_analysis_executor.py @@ -6,11 +6,47 @@ import asyncio from libs.agent_framework.groupchat_orchestrator import OrchestrationResult -from steps.analysis.models.step_output import Analysis_BooleanExtendedResult +from steps.analysis.models.step_output import ( + AnalysisOutput, + Analysis_BooleanExtendedResult, + ComplexityAnalysis, + FileType, + MigrationReadiness, +) from steps.analysis.models.step_param import Analysis_TaskParam from steps.analysis.workflow.analysis_executor import AnalysisExecutor +def _make_analysis_output(process_id: str) -> AnalysisOutput: + return AnalysisOutput( + process_id=process_id, + platform_detected="EKS", + confidence_score="95%", + files_discovered=[ + FileType( + filename="app.yaml", + type="Deployment", + complexity="Low", + azure_mapping="AKS Deployment", + ) + ], + complexity_analysis=ComplexityAnalysis( + network_complexity="Low", + security_complexity="Low", + storage_complexity="Low", + compute_complexity="Low", + ), + migration_readiness=MigrationReadiness( + overall_score="A", + concerns=[], + recommendations=[], + ), + summary="ok", + expert_insights=[], + analysis_file="analysis.md", + ) + + class _FakeTelemetry: def __init__(self): self.transitions: list[tuple[str, str, str]] = [] @@ -59,6 +95,7 @@ async def execute(self, task_param=None): result=True, is_hard_terminated=False, process_id=task_param.process_id, + output=_make_analysis_output(task_param.process_id), ), ) @@ -143,3 +180,63 @@ async def execute(self, task_param=None): assert isinstance(ctx.yielded[0], Analysis_BooleanExtendedResult) asyncio.run(_run()) + + +def test_analysis_executor_raises_when_soft_completion_has_no_output(monkeypatch): + """Soft completion with output=None is incoherent: AnalysisExecutor must raise. + + This guards against ResultGenerator returning a self-contradictory shell + (success=True, is_hard_terminated=False, output=None) which would otherwise + propagate to Design and crash there with `NoneType.process_id`. + """ + async def _run(): + import pytest + + telemetry = _FakeTelemetry() + app_context = _FakeAppContext(telemetry) + ctx = _FakeCtx() + + class _FakeOrchestrator: + def __init__(self, _app_context): + pass + + async def execute(self, task_param=None): + return OrchestrationResult( + success=True, + conversation=[], + agent_responses=[], + tool_usage={}, + result=Analysis_BooleanExtendedResult( + result=True, + is_hard_terminated=False, + process_id=task_param.process_id, + reason="agents never produced output", + ), + ) + + monkeypatch.setattr( + "steps.analysis.workflow.analysis_executor.text2art", + lambda _s: "ART", + raising=False, + ) + monkeypatch.setattr( + "steps.analysis.workflow.analysis_executor.AnalysisOrchestrator", + _FakeOrchestrator, + ) + + executor = AnalysisExecutor(id="analysis", app_context=app_context) + message = Analysis_TaskParam( + process_id="p1", + container_name="c1", + source_file_folder="p1/source", + workspace_file_folder="p1/workspace", + output_file_folder="p1/output", + ) + + with pytest.raises(Exception, match="produced no AnalysisOutput"): + await executor.handle_execute(message, ctx) # type: ignore[arg-type] + + assert len(ctx.sent) == 0 + assert len(ctx.yielded) == 0 + + asyncio.run(_run()) diff --git a/src/processor/src/tests/unit/steps/convert/test_yaml_convert_executor.py b/src/processor/src/tests/unit/steps/convert/test_yaml_convert_executor.py index d1957289..2a12d3b7 100644 --- a/src/processor/src/tests/unit/steps/convert/test_yaml_convert_executor.py +++ b/src/processor/src/tests/unit/steps/convert/test_yaml_convert_executor.py @@ -6,11 +6,63 @@ import asyncio from libs.agent_framework.groupchat_orchestrator import OrchestrationResult -from steps.convert.models.step_output import Yaml_ExtendedBooleanResult +from steps.convert.models.step_output import ( + ConvertedFile, + ConversionMetrics, + ConversionQuality, + DimensionalAnalysis, + MultiDimensionalAnalysis, + YamlOutput, + Yaml_ExtendedBooleanResult, +) from steps.convert.workflow.yaml_convert_executor import YamlConvertExecutor from steps.design.models.step_output import Design_ExtendedBooleanResult +def _make_yaml_output() -> YamlOutput: + dim = DimensionalAnalysis( + complexity="Low", + converted_components=["pod"], + azure_optimizations="none", + concerns=[], + success_rate="100%", + ) + return YamlOutput( + converted_files=[ + ConvertedFile( + source_file="a.yaml", + converted_file="a-azure.yaml", + conversion_status="Success", + accuracy_rating="100%", + concerns=[], + azure_enhancements=[], + ) + ], + multi_dimensional_analysis=MultiDimensionalAnalysis( + network_analysis=dim, + security_analysis=dim, + storage_analysis=dim, + compute_analysis=dim, + ), + overall_conversion_metrics=ConversionMetrics( + total_files=1, + successful_conversions=1, + failed_conversions=0, + overall_accuracy="100%", + azure_compatibility="100%", + ), + conversion_quality=ConversionQuality( + azure_best_practices="ok", + security_hardening="ok", + performance_optimization="ok", + production_readiness="ok", + ), + summary="ok", + expert_insights=[], + conversion_report_file="report.md", + ) + + class _FakeTelemetry: def __init__(self): self.transitions: list[tuple[str, str, str]] = [] @@ -59,6 +111,7 @@ async def execute(self, task_param=None): result=True, is_hard_terminated=False, process_id=task_param.process_id, + termination_output=_make_yaml_output(), ), ) @@ -118,3 +171,47 @@ async def execute(self, task_param=None): assert isinstance(ctx.yielded[0], Yaml_ExtendedBooleanResult) asyncio.run(_run()) + + +def test_yaml_convert_executor_raises_when_soft_completion_has_no_output(monkeypatch): + """Soft completion with termination_output=None is incoherent: must raise.""" + async def _run(): + import pytest + + telemetry = _FakeTelemetry() + app_context = _FakeAppContext(telemetry) + ctx = _FakeCtx() + + class _FakeOrchestrator: + def __init__(self, _app_context): + pass + + async def execute(self, task_param=None): + return OrchestrationResult( + success=True, + conversation=[], + agent_responses=[], + tool_usage={}, + result=Yaml_ExtendedBooleanResult( + result=True, + is_hard_terminated=False, + process_id=task_param.process_id, + reason="agents never produced output", + ), + ) + + monkeypatch.setattr( + "steps.convert.workflow.yaml_convert_executor.YamlConvertOrchestrator", + _FakeOrchestrator, + ) + + executor = YamlConvertExecutor(id="yaml", app_context=app_context) + message = Design_ExtendedBooleanResult(process_id="p1") + + with pytest.raises(Exception, match="produced no YAML conversion output"): + await executor.handle_execute(message, ctx) # type: ignore[arg-type] + + assert len(ctx.sent) == 0 + assert len(ctx.yielded) == 0 + + asyncio.run(_run()) diff --git a/src/processor/src/tests/unit/steps/design/test_design_executor.py b/src/processor/src/tests/unit/steps/design/test_design_executor.py index c03b93b0..f782a794 100644 --- a/src/processor/src/tests/unit/steps/design/test_design_executor.py +++ b/src/processor/src/tests/unit/steps/design/test_design_executor.py @@ -7,10 +7,24 @@ from libs.agent_framework.groupchat_orchestrator import OrchestrationResult from steps.analysis.models.step_output import Analysis_BooleanExtendedResult -from steps.design.models.step_output import Design_ExtendedBooleanResult +from steps.design.models.step_output import ( + DesignOutput, + Design_ExtendedBooleanResult, + OutputFile, +) from steps.design.workflow.design_executor import DesignExecutor +def _make_design_output() -> DesignOutput: + return DesignOutput( + result="Success", + summary="ok", + azure_services=["AKS"], + architecture_decisions=["use managed identity"], + outputs=[OutputFile(file="design.md", description="design doc")], + ) + + class _FakeTelemetry: def __init__(self): self.transitions: list[tuple[str, str, str]] = [] @@ -59,6 +73,7 @@ async def execute(self, task_param=None): result=True, is_hard_terminated=False, process_id=task_param.process_id, + termination_output=_make_design_output(), ), ) @@ -118,3 +133,47 @@ async def execute(self, task_param=None): assert isinstance(ctx.yielded[0], Design_ExtendedBooleanResult) asyncio.run(_run()) + + +def test_design_executor_raises_when_soft_completion_has_no_output(monkeypatch): + """Soft completion with termination_output=None is incoherent: must raise.""" + async def _run(): + import pytest + + telemetry = _FakeTelemetry() + app_context = _FakeAppContext(telemetry) + ctx = _FakeCtx() + + class _FakeOrchestrator: + def __init__(self, _app_context): + pass + + async def execute(self, task_param=None): + return OrchestrationResult( + success=True, + conversation=[], + agent_responses=[], + tool_usage={}, + result=Design_ExtendedBooleanResult( + result=True, + is_hard_terminated=False, + process_id=task_param.process_id, + reason="agents never produced output", + ), + ) + + monkeypatch.setattr( + "steps.design.workflow.design_executor.DesignOrchestrator", + _FakeOrchestrator, + ) + + executor = DesignExecutor(id="design", app_context=app_context) + message = Analysis_BooleanExtendedResult(process_id="p1") + + with pytest.raises(Exception, match="produced no DesignOutput"): + await executor.handle_execute(message, ctx) # type: ignore[arg-type] + + assert len(ctx.sent) == 0 + assert len(ctx.yielded) == 0 + + asyncio.run(_run()) diff --git a/src/processor/src/tests/unit/steps/documentation/test_documentation_executor.py b/src/processor/src/tests/unit/steps/documentation/test_documentation_executor.py index 9d9ff63e..29f0c95c 100644 --- a/src/processor/src/tests/unit/steps/documentation/test_documentation_executor.py +++ b/src/processor/src/tests/unit/steps/documentation/test_documentation_executor.py @@ -7,10 +7,40 @@ from libs.agent_framework.groupchat_orchestrator import OrchestrationResult from steps.convert.models.step_output import Yaml_ExtendedBooleanResult -from steps.documentation.models.step_output import Documentation_ExtendedBooleanResult +from steps.documentation.models.step_output import ( + AggregatedResults, + DocumentationOutput, + Documentation_ExtendedBooleanResult, + ExpertCollaboration, + GeneratedFilesCollection, + ProcessMetrics, +) from steps.documentation.workflow.documentation_executor import DocumentationExecutor +def _make_documentation_output() -> DocumentationOutput: + return DocumentationOutput( + aggregated_results=AggregatedResults( + executive_summary="ok", + total_files_processed=1, + overall_success_rate="100%", + platform_detected="EKS", + conversion_accuracy="100%", + documentation_completeness="100%", + enterprise_readiness="ok", + ), + generated_files=GeneratedFilesCollection(), + expert_collaboration=ExpertCollaboration(), + process_metrics=ProcessMetrics( + platform_detected="EKS", + conversion_accuracy="100%", + documentation_completeness="100%", + enterprise_readiness="ok", + ), + summary="ok", + ) + + class _FakeTelemetry: def __init__(self): self.transitions: list[tuple[str, str, str]] = [] @@ -54,6 +84,7 @@ async def execute(self, task_param=None): result=Documentation_ExtendedBooleanResult( result=True, process_id=task_param.process_id, + termination_output=_make_documentation_output(), ), ) @@ -71,3 +102,46 @@ async def execute(self, task_param=None): assert isinstance(ctx.yielded[0], Documentation_ExtendedBooleanResult) asyncio.run(_run()) + + +def test_documentation_executor_raises_when_soft_completion_has_no_output(monkeypatch): + """Soft completion with termination_output=None is incoherent: must raise.""" + async def _run(): + import pytest + + telemetry = _FakeTelemetry() + app_context = _FakeAppContext(telemetry) + ctx = _FakeCtx() + + class _FakeOrchestrator: + def __init__(self, _app_context): + pass + + async def execute(self, task_param=None): + return OrchestrationResult( + success=True, + conversation=[], + agent_responses=[], + tool_usage={}, + result=Documentation_ExtendedBooleanResult( + result=True, + is_hard_terminated=False, + process_id=task_param.process_id, + reason="agents never produced output", + ), + ) + + monkeypatch.setattr( + "steps.documentation.workflow.documentation_executor.DocumentationOrchestrator", + _FakeOrchestrator, + ) + + executor = DocumentationExecutor(id="documentation", app_context=app_context) + message = Yaml_ExtendedBooleanResult(process_id="p1") + + with pytest.raises(Exception, match="produced no DocumentationOutput"): + await executor.handle_execute(message, ctx) # type: ignore[arg-type] + + assert len(ctx.yielded) == 0 + + asyncio.run(_run()) From f74629ba53c8722c4e3b04738389c9cd8b9ecb2f Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Sat, 13 Jun 2026 14:04:09 +0530 Subject: [PATCH 31/50] fix(groupchat): resolve agent identity via author_name for af 1.3.0 Root cause of the "Runner did not converge after 100 iterations" production failure (and the Chief-Architect-only loop that preceded it): agent-framework 1.3.0 changed how AgentResponseUpdate is constructed. `map_chat_to_agent_update` (_types.py:2825-2837) now only sets `author_name` and leaves `agent_id` as None. Our orchestrator was reading `event.agent_id` exclusively, so every streaming update resolved to `agent_name=""`. That silently broke: * Loop detection (line 1080 `if agent_name == self.coordinator_name` never matched, so the streak counter never advanced and the 3x same-agent guard never fired). Production looped 100x on Chief Architect with zero detection. * Coordinator termination signal extraction (`finish=true`, `instruction=complete`, blocking instructions) - same gated block. * Manager-instruction parsing for the next participant. The [MEMORY] logs continued to show real agent names ("Chief Architect") because `SharedMemoryContextProvider` reads the name from the agent's own context, not from the workflow event - which is why the regression was invisible from logs alone. Fix: in `_handle_agent_update`, prefer `event.author_name` (which IS populated by 1.3.0's `map_chat_to_agent_update`) and fall back to `agent_id` only when author_name is missing, for backwards compat with older event shapes. Use `getattr` defensively so existing tests that construct SimpleNamespace events without author_name still work. Tests: * test_handle_agent_update_resolves_coordinator_via_author_name_when_agent_id_is_none - asserts the identity resolution itself * test_loop_detection_fires_on_3_consecutive_coordinator_selections_via_handle_agent_update - end-to-end through the production code path: 3 identical Coordinator selections via _handle_agent_update must trip _forced_termination * Both tests verified to FAIL without the fix (intentionally reverted to confirm) and PASS with the fix * Full suite: 831 passed (was 829, +2 regression tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 13 ++- ...test_groupchat_orchestrator_termination.py | 106 ++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 05034ddd..b92ddd88 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -727,7 +727,18 @@ async def _handle_agent_update( 3. Trigger callback with complete response 4. Handle tool calls separately from text streaming """ - agent_name = self._normalize_executor_id(event.agent_id or "") + # NOTE: In agent-framework 1.3.0, ``AgentResponseUpdate.agent_id`` is no + # longer populated by ``map_chat_to_agent_update`` (only ``author_name`` + # is set, from the agent's name). Reading ``event.agent_id`` alone + # silently yielded an empty string, which made every downstream identity + # check (loop detection, coordinator termination signal extraction, + # manager-instruction parsing) silently no-op. Prefer ``author_name`` + # and fall back to ``agent_id`` only for older shapes. Use ``getattr`` + # so older event types without ``author_name`` still work. + author_name = getattr(event, "author_name", None) + agent_name = author_name or self._normalize_executor_id( + getattr(event, "agent_id", None) or "" + ) await self._start_agent_if_needed(agent_name, stream_callback, callback) self._append_text_chunk(event) await self._process_tool_calls(event, agent_name, stream_callback) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py index 36a0c00b..5d98a0d0 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py @@ -127,3 +127,109 @@ def _agent_reply(text: str = "ok"): assert orch._forced_termination_requested is False asyncio.run(_run()) + + +@dataclass +class _AgentResponseUpdateStub: + """Mimics the agent-framework 1.3.0 AgentResponseUpdate shape. + + Only the fields actually read by ``_handle_agent_update`` / + ``_normalize_executor_id`` matter. In 1.3.0 ``agent_id`` is no longer + populated by ``map_chat_to_agent_update`` - only ``author_name`` is set. + This stub reproduces that shape. + """ + + author_name: str | None = None + agent_id: str | None = None + contents: list = None # type: ignore[assignment] + + def __post_init__(self): + if self.contents is None: + self.contents = [] + + +def test_handle_agent_update_resolves_coordinator_via_author_name_when_agent_id_is_none(): + """Regression guard for agent-framework 1.3.0. + + In 1.3.0 ``AgentResponseUpdate.agent_id`` is ``None`` because + ``map_chat_to_agent_update`` only sets ``author_name``. Reading + ``event.agent_id`` alone silently produced an empty string, so + ``agent_name == self.coordinator_name`` never matched and loop + detection / coordinator termination signal extraction silently + no-opped. The orchestrator must treat ``author_name`` as the + authoritative source. + """ + + async def _run(): + orch = _make_orchestrator() + + event = _AgentResponseUpdateStub( + author_name="Coordinator", + agent_id=None, + ) + + # No-op tool/text processing: we only care about agent identity. + await orch._handle_agent_update(event, stream_callback=None, callback=None) # type: ignore[arg-type] + + assert orch._last_executor_id == "Coordinator", ( + "author_name must be used to identify the agent; otherwise " + "_last_executor_id stays empty and downstream coordinator " + "checks silently fail." + ) + + asyncio.run(_run()) + + +def test_loop_detection_fires_on_3_consecutive_coordinator_selections_via_handle_agent_update(): + """End-to-end check: feeding 3 identical Coordinator selections through + ``_handle_agent_update`` (the path used in production) must trigger the + loop-detection forced termination. This is the path that was silently + broken in the 1.3.0 regression. + """ + + async def _run(): + orch = _make_orchestrator() + orch._conversation = [] + + coordinator_json = json.dumps( + { + "selected_participant": "Chief Architect", + "instruction": "re-list", + "finish": False, + "final_message": "", + } + ) + + # Simulate three consecutive Coordinator turns, each emitting the + # same selection. Between each Coordinator turn we drive an update + # from a non-Coordinator agent so the orchestrator's "agent switch" + # logic completes the previous Coordinator response (which is what + # actually runs loop-detection at line 1080). + for _ in range(3): + # Coordinator emits its selection as a streaming chunk. + await orch._handle_agent_update( + _AgentResponseUpdateStub(author_name="Coordinator"), + stream_callback=None, + callback=None, + ) # type: ignore[arg-type] + orch._current_agent_response = [coordinator_json] + + # Then Chief Architect emits a chunk: the agent switch closes + # out the Coordinator response and runs loop detection. + await orch._handle_agent_update( + _AgentResponseUpdateStub(author_name="Chief Architect"), + stream_callback=None, + callback=None, + ) # type: ignore[arg-type] + orch._current_agent_response = ["ack"] + + # Closing the final Chief Architect response keeps state consistent. + await orch._complete_agent_response("Chief Architect", callback=None) + + assert orch._forced_termination_requested is True, ( + "Loop detection failed to fire after 3 identical Coordinator " + "selections via _handle_agent_update; agent identity resolution " + "is broken." + ) + + asyncio.run(_run()) From 42902ab23491133d4b6c5857b0e5fca13e372191 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Sat, 13 Jun 2026 14:33:34 +0530 Subject: [PATCH 32/50] fix(groupchat): route WorkflowEvent payloads + enforce framework max_rounds for af 1.3.0 In agent-framework 1.3.0, `workflow.run(stream=True)` only yields `WorkflowEvent` instances. `AgentResponseUpdate` is wrapped inside `event.data` for `type=="output"` events. The two types are unrelated (verified by MRO), so the previous `isinstance(event, AgentResponseUpdate)` gate from the b260107 era was permanently dead in 1.3.0. As a result every orchestrator-side safety guard inside that branch silently no-opped: * per-agent loop detection * Coordinator finish=true detection * max_rounds enforcement * streaming callback dispatch * manager-instruction extraction That is why production runs hit the framework's own 100-iteration runner cap as `RuntimeError("Runner did not converge after 100 iterations")` even after the recent identity-resolution patch (which only touched code that never executed). Three coordinated fixes: 1. Replace the dead `isinstance(event, AgentResponseUpdate)` gate with `isinstance(event, WorkflowEvent) and event.type == "output"` and inspect `event.data` / `event.executor_id` to distinguish per- participant streaming chunks (executor_id matches one of self.agents and data is AgentResponseUpdate) from the framework orchestrator's final output (list[Message] or custom result object). 2. Add `executor_id` parameter to `_handle_agent_update` so identity resolves from the WorkflowEvent wrapper's executor_id (always populated from `AgentExecutor.id` = the agent's name) first, then falls back to `event.author_name`, then legacy `event.agent_id`. Matches the approach already used by Content Processing Solution. 3. Pass `max_rounds=self.max_rounds` and `intermediate_outputs=True` to `GroupChatBuilder`: - `max_rounds` gives the framework itself a clean termination ceiling so even if our orchestrator-side guards miss, the workflow halts cleanly instead of crashing at the runner's 100-iteration cap. - `intermediate_outputs=True` is required for each participant's `yield_output(AgentResponseUpdate)` call to surface as a workflow `output` event. Without this, only the orchestrator's final yield reaches our streaming loop and the per-agent guards above never run. Tests: * Existing termination/loop-detection tests still pass (handler now has 3-tier identity resolution with backward-compat for `author_name`). * Added `test_handle_agent_update_prefers_executor_id_over_author_name` to lock in the new precedence. * Added `test_handle_agent_update_strips_executor_id_prefix` to cover the `groupchat_agent:Coordinator` framework prefix. * Full suite: 833 passed (was 831; +2 new tests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 99 +++++++++++++------ ...test_groupchat_orchestrator_termination.py | 70 ++++++++++++- 2 files changed, 135 insertions(+), 34 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index b92ddd88..94079d93 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -543,9 +543,32 @@ async def run_stream( termination_type="hard_timeout", ) - if isinstance(event, AgentResponseUpdate): + # In agent-framework 1.3.0, ``workflow.run(stream=True)`` yields + # only ``WorkflowEvent`` instances; ``AgentResponseUpdate`` is + # wrapped inside ``WorkflowEvent.data`` for ``type=="output"`` + # events. The previous ``isinstance(event, AgentResponseUpdate)`` + # check from the b260107 era is permanently dead in 1.3.0 + # because the two types are unrelated. We now dispatch on + # ``WorkflowEvent.type`` and inspect ``event.data`` / + # ``event.executor_id`` to route per-participant streaming + # chunks vs the orchestrator's final output. + if not isinstance(event, WorkflowEvent) or event.type != "output": + continue + + data = event.data + src_executor = self._normalize_executor_id(event.executor_id or "") + + # Per-participant streaming chunk. Requires + # ``intermediate_outputs=True`` on the GroupChatBuilder so the + # underlying executors' ``yield_output(AgentResponseUpdate)`` + # calls surface as workflow events rather than being swallowed. + if ( + isinstance(data, AgentResponseUpdate) + and src_executor in self.agents + ): await self._handle_agent_update( - event, + data, + executor_id=event.executor_id, stream_callback=on_agent_response_stream, callback=on_agent_response, ) @@ -565,22 +588,23 @@ async def run_stream( # If the Coordinator requested finish=true, stop immediately. if self._termination_requested: break - elif event.type == "output": - event: WorkflowEvent - # Complete last agent's response before finishing - if self._last_executor_id and self._current_agent_response: - await self._complete_agent_response( - self._last_executor_id, on_agent_response - ) - # Extract final conversation from output - if isinstance(event.data, list): - conversation = event.data - self._conversation = conversation # Update instance variable - else: - # Handle custom result objects with conversation attribute - conversation = getattr(event.data, "conversation", []) - self._conversation = conversation # Update instance variable + continue + + # Final orchestrator output: complete any buffered agent + # response and capture the conversation. + if self._last_executor_id and self._current_agent_response: + await self._complete_agent_response( + self._last_executor_id, on_agent_response + ) + + if isinstance(data, list): + conversation = data + self._conversation = conversation # Update instance variable + else: + # Handle custom result objects with conversation attribute + conversation = getattr(data, "conversation", []) + self._conversation = conversation # Update instance variable # Backfill tool usage from the final conversation (more reliable than streaming updates) # AgentResponseUpdate may stream text only; tool calls are represented as FunctionCallContent @@ -715,6 +739,7 @@ async def run_stream( async def _handle_agent_update( self, event: AgentResponseUpdate, + executor_id: str | None = None, stream_callback: AgentResponseStreamCallback | None = None, callback: AgentResponseCallback | None = None, ) -> None: @@ -726,19 +751,21 @@ async def _handle_agent_update( 2. On agent switch, complete previous agent's response 3. Trigger callback with complete response 4. Handle tool calls separately from text streaming + + Agent identity resolution priority: + 1. ``executor_id`` from the wrapping ``WorkflowEvent`` (always + populated by the workflow runner from ``AgentExecutor.id`` which + is the agent's name). This is the primary source in 1.3.0. + 2. ``event.author_name`` (set by 1.3.0's ``map_chat_to_agent_update``). + 3. ``event.agent_id`` (legacy; not populated in 1.3.0). """ - # NOTE: In agent-framework 1.3.0, ``AgentResponseUpdate.agent_id`` is no - # longer populated by ``map_chat_to_agent_update`` (only ``author_name`` - # is set, from the agent's name). Reading ``event.agent_id`` alone - # silently yielded an empty string, which made every downstream identity - # check (loop detection, coordinator termination signal extraction, - # manager-instruction parsing) silently no-op. Prefer ``author_name`` - # and fall back to ``agent_id`` only for older shapes. Use ``getattr`` - # so older event types without ``author_name`` still work. - author_name = getattr(event, "author_name", None) - agent_name = author_name or self._normalize_executor_id( - getattr(event, "agent_id", None) or "" - ) + if executor_id: + agent_name = self._normalize_executor_id(executor_id) + else: + author_name = getattr(event, "author_name", None) + agent_name = author_name or self._normalize_executor_id( + getattr(event, "agent_id", None) or "" + ) await self._start_agent_if_needed(agent_name, stream_callback, callback) self._append_text_chunk(event) await self._process_tool_calls(event, agent_name, stream_callback) @@ -1237,10 +1264,24 @@ async def _build_groupchat(self) -> Workflow: and name != self.get_result_generator_name() ] + # ``max_rounds`` is enforced at the framework level so the workflow + # halts cleanly even if our orchestrator-side guards miss an event + # shape. Without this, the framework's default behavior is "continue + # indefinitely" (see GroupChatBuilder docstring) until the workflow + # runner hits its own 100-iteration cap and raises + # ``RuntimeError("Runner did not converge after 100 iterations")``. + # + # ``intermediate_outputs=True`` surfaces each participant's + # ``yield_output(AgentResponseUpdate)`` call as a workflow ``output`` + # event. Without this, only the orchestrator's final yield reaches + # our streaming loop, which means per-agent loop detection, finish + # signal extraction, and streaming callbacks all silently no-op. return ( GroupChatBuilder( orchestrator_agent=coordinator, participants=participants, + max_rounds=self.max_rounds, + intermediate_outputs=True, ) .build() ) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py index 5d98a0d0..73eb7e6b 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py @@ -227,9 +227,69 @@ async def _run(): await orch._complete_agent_response("Chief Architect", callback=None) assert orch._forced_termination_requested is True, ( - "Loop detection failed to fire after 3 identical Coordinator " - "selections via _handle_agent_update; agent identity resolution " - "is broken." - ) - + "Loop detection failed to fire after 3 identical Coordinator " + "selections via _handle_agent_update; agent identity resolution " + "is broken." + ) + + asyncio.run(_run()) + + +def test_handle_agent_update_prefers_executor_id_over_author_name(): + """In agent-framework 1.3.0, the workflow runner always wraps payloads in + a ``WorkflowEvent`` whose ``executor_id`` is the ``AgentExecutor.id`` + (= the agent's name). This is the most reliable identity source - more + reliable than ``author_name`` which may differ if the agent runtime + rewrites the chat author. The handler must prefer ``executor_id`` when + provided. + """ + + async def _run(): + orch = _make_orchestrator() + + # author_name disagrees with the framework executor_id on purpose. + event = _AgentResponseUpdateStub( + author_name="SomethingElse", + agent_id=None, + ) + + await orch._handle_agent_update( + event, + executor_id="Coordinator", + stream_callback=None, + callback=None, + ) # type: ignore[arg-type] + + assert orch._last_executor_id == "Coordinator", ( + "executor_id from the WorkflowEvent wrapper must take precedence " + "over event.author_name; otherwise downstream coordinator checks " + "may resolve to the wrong agent." + ) + + asyncio.run(_run()) + + +def test_handle_agent_update_strips_executor_id_prefix(): + """``GroupChatBuilder`` may wrap executor ids with a + ``groupchat_agent:Coordinator`` prefix. ``_normalize_executor_id`` must + strip it so the agent name compares cleanly against ``coordinator_name``. + """ + + async def _run(): + orch = _make_orchestrator() + + event = _AgentResponseUpdateStub(author_name=None, agent_id=None) + + await orch._handle_agent_update( + event, + executor_id="groupchat_agent:Coordinator", + stream_callback=None, + callback=None, + ) # type: ignore[arg-type] + + assert orch._last_executor_id == "Coordinator", ( + "_normalize_executor_id must strip the framework prefix so " + "agent identity matches the configured coordinator_name." + ) + asyncio.run(_run()) From 7a0212fdaa6594dc8383327e95cf9661c7e200f5 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Sat, 13 Jun 2026 15:36:22 +0530 Subject: [PATCH 33/50] fix(groupchat): detect participant loops via executor_completed events In agent-framework 1.3.0 the GroupChat orchestrator agent (Coordinator) is invoked directly inside the framework's internal _invoke_agent_helper (agent_framework_orchestrations/_group_chat.py:484) rather than through an AgentExecutor. The Coordinator therefore never surfaces as a workflow event, which makes our existing Coordinator-JSON-based loop detector in _complete_agent_response permanently dead in 1.3.0. Symptom in production: workflow loops with the Coordinator latched onto the same participant (e.g., Chief Architect repeatedly asked to produce an Evidence Pack that never satisfies the next reviewer). The loop runs until the framework's max_rounds ceiling fires (~17 min at default 100) instead of being caught early. Fix: * Track participant turn completions from WorkflowEvent.executor_completed, the one observable signal that does NOT depend on Coordinator visibility (participants ARE wrapped in AgentExecutor and so do emit these events). * Force-terminate (hard_loop) after 3 consecutive completions of the same participant. * Force-terminate (hard_timeout) when total participant completions reach max_rounds; independent of len(agent_responses) which only grows on agent switch and so can never reach max_rounds during a same-participant loop. * Flush per-participant streaming buffer on each executor_completed so back-to-back same-agent turns produce one AgentResponse per turn instead of accumulating across turns. * Move forced-termination break check to top of the streaming loop so any branch (timeout, participant loop, Coordinator finish=true) takes effect on the very next event rather than waiting for the next output event. Adds 3 regression tests covering the streak trigger, the alternation reset, and the round-budget enforcement. 836 tests pass (833 -> 836). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 163 +++++++++++++++++- ...test_groupchat_orchestrator_termination.py | 110 ++++++++++++ 2 files changed, 264 insertions(+), 9 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 94079d93..3185f6eb 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -313,6 +313,33 @@ def __init__( # Snapshot of progress_counter at the time we last saw _last_coordinator_selection. self._last_coordinator_selection_progress: int = 0 + # Per-participant turn tracking driven by ``WorkflowEvent.executor_completed``. + # + # In agent-framework 1.3.0 the GroupChat orchestrator agent (the + # Coordinator) is invoked directly inside the framework's internal + # ``_invoke_agent_helper`` (see + # ``agent_framework_orchestrations/_group_chat.py:484``). It is NOT + # wrapped in an ``AgentExecutor`` and therefore never surfaces as a + # workflow event - which makes the Coordinator-JSON-based loop + # detection in ``_complete_agent_response`` permanently dead in 1.3.0. + # + # The only observable "the conversation is moving" pulse we have is + # ``executor_completed`` events for the *participants* (which DO go + # through ``AgentExecutor``). We track: + # - the most recently completed participant, + # - the streak of consecutive completions of that participant, + # - the total number of participant turns, + # and use these for two safety nets in the streaming loop: + # * 3+ consecutive same-participant turns => hard_loop termination + # * total turns >= ``max_rounds`` => hard_timeout termination + # (independent of ``len(self.agent_responses)`` which only grows on + # agent switch and so cannot reach ``max_rounds`` during a same- + # participant loop). + self._participant_completions_total: int = 0 + self._last_completed_participant: str | None = None + self._participant_completion_streak: int = 0 + self._participant_consecutive_loop_threshold: int = 3 + def _request_forced_termination( self, *, reason: str, termination_type: str ) -> None: @@ -543,6 +570,15 @@ async def run_stream( termination_type="hard_timeout", ) + # Honor any pending termination request at the *top* of each + # iteration so that branches which set the flags (timeout, + # participant loop detection, Coordinator finish=true) take + # effect immediately on the next event - rather than being + # gated on the next ``output`` event arriving (which during a + # slow loop can be many seconds away). + if self._forced_termination_requested or self._termination_requested: + break + # In agent-framework 1.3.0, ``workflow.run(stream=True)`` yields # only ``WorkflowEvent`` instances; ``AgentResponseUpdate`` is # wrapped inside ``WorkflowEvent.data`` for ``type=="output"`` @@ -552,7 +588,46 @@ async def run_stream( # ``WorkflowEvent.type`` and inspect ``event.data`` / # ``event.executor_id`` to route per-participant streaming # chunks vs the orchestrator's final output. - if not isinstance(event, WorkflowEvent) or event.type != "output": + if not isinstance(event, WorkflowEvent): + continue + + # Participant turn completion. Used for loop / max_rounds + # safety nets that work even when the Coordinator is + # invisible to the streaming loop (which it is in 1.3.0 - + # the Coordinator runs inside the framework's internal + # ``_invoke_agent_helper`` and never surfaces as an executor + # event). See ``_track_participant_completion`` for details. + if event.type == "executor_completed": + src_executor = self._normalize_executor_id( + event.executor_id or "" + ) + if ( + src_executor in self.agents + and src_executor != self.coordinator_name + and src_executor != self.get_result_generator_name() + ): + # Flush this participant's streaming buffer into a + # discrete per-turn ``AgentResponse`` before we track + # the completion. Without this, when the framework's + # Coordinator picks the same participant back-to-back + # (the loop pattern we're trying to detect), + # ``_start_agent_if_needed`` sees no agent switch on + # the NEXT turn's chunks and the buffer would grow + # across turns - producing one merged response rather + # than one response per turn. + if ( + self._last_executor_id == src_executor + and self._current_agent_response + ): + await self._complete_agent_response( + src_executor, on_agent_response + ) + self._current_agent_response = [] + self._last_executor_id = None + self._track_participant_completion(src_executor) + continue + + if event.type != "output": continue data = event.data @@ -573,7 +648,12 @@ async def run_stream( callback=on_agent_response, ) - # Enforce max rounds as a safety guard. + # Secondary max_rounds safety net based on agent switches. + # The primary check lives in ``_track_participant_completion`` + # (driven by ``executor_completed`` events) and works even + # when the same agent runs back-to-back. This switch-based + # check is kept as defense-in-depth for sessions with + # normal alternation. if self.max_rounds and len(self.agent_responses) >= self.max_rounds: self._request_forced_termination( reason=( @@ -582,13 +662,9 @@ async def run_stream( termination_type="hard_timeout", ) - if self._forced_termination_requested: - break - - # If the Coordinator requested finish=true, stop immediately. - if self._termination_requested: - break - + # Termination flags are honored at the top of the next + # iteration so any branch can request termination + # uniformly without duplicating break logic here. continue # Final orchestrator output: complete any buffered agent @@ -777,6 +853,75 @@ def _normalize_executor_id(self, executor_id: str) -> str: """ return executor_id.split(":")[-1] + def _track_participant_completion(self, src_executor: str) -> None: + """Track a participant turn completion for loop / max_rounds detection. + + Called from the streaming loop on every ``WorkflowEvent.type == + "executor_completed"`` event whose ``executor_id`` matches one of our + registered non-Coordinator, non-ResultGenerator participants. + + Why this exists (agent-framework 1.3.0 design constraint): + The framework's ``GroupChatBuilder.orchestrator_agent`` (our + Coordinator) is invoked directly via ``self._agent.run(...)`` + inside ``agent_framework_orchestrations/_group_chat.py:484``. It + is NOT wrapped in an ``AgentExecutor`` and therefore never + surfaces as a workflow event. Our existing Coordinator-JSON-based + loop detector in ``_complete_agent_response`` (lines ~1118-1181) + is consequently permanently dead in 1.3.0. We need an independent + loop signal that does NOT rely on Coordinator visibility. + + Two safety nets enforced here: + + 1. Same-participant streak (``_participant_consecutive_loop_threshold``, + default 3): if the Coordinator keeps selecting the same participant + (e.g., the Chief Architect latched on producing an Evidence Pack + that never satisfies the next reviewer), 3+ consecutive completions + of the same participant force-terminate with ``hard_loop``. + + 2. Total round budget: each participant turn counts as one round. + Once total completions reach ``self.max_rounds`` the workflow + force-terminates with ``hard_timeout``. This is independent of + ``len(self.agent_responses)`` (which only grows on agent switch + via ``_start_agent_if_needed`` and therefore cannot reach + ``max_rounds`` during a same-participant loop). + """ + if src_executor == self._last_completed_participant: + self._participant_completion_streak += 1 + else: + self._last_completed_participant = src_executor + self._participant_completion_streak = 1 + self._participant_completions_total += 1 + + if ( + self._participant_completion_streak + >= self._participant_consecutive_loop_threshold + ): + self._request_forced_termination( + reason=( + f"Loop detected: participant '{src_executor}' completed " + f"{self._participant_completion_streak} consecutive turns " + "with no other participant in between (Coordinator is " + "stuck on the same selection; in agent-framework 1.3.0 " + "the Coordinator runs inside the framework and is " + "invisible to the streaming loop, so we infer this from " + "executor_completed events)" + ), + termination_type="hard_loop", + ) + return + + if ( + self.max_rounds + and self._participant_completions_total >= self.max_rounds + ): + self._request_forced_termination( + reason=( + f"Workflow exceeded max_rounds={self.max_rounds} " + "participant turns; terminating to avoid infinite loop" + ), + termination_type="hard_timeout", + ) + async def _start_agent_if_needed( self, agent_name: str, diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py index 73eb7e6b..2bd88d65 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py @@ -293,3 +293,113 @@ async def _run(): ) asyncio.run(_run()) + + +def test_participant_completion_streak_triggers_forced_termination(): + """In agent-framework 1.3.0 the GroupChat orchestrator agent (Coordinator) + is invoked directly inside the framework's ``_invoke_agent_helper`` and + is NOT wrapped in an ``AgentExecutor``, so it never surfaces as a + workflow event. The Coordinator-JSON loop detector in + ``_complete_agent_response`` is therefore permanently dead in 1.3.0. + + The only observable loop signal we have is consecutive + ``executor_completed`` events for the same participant. After + ``_participant_consecutive_loop_threshold`` (default 3) same-participant + completions, the orchestrator must force-terminate with ``hard_loop`` + so the workflow halts cleanly instead of running until the framework's + own max_rounds ceiling (which at default 100 is ~17 min). + """ + + async def _run(): + orch = _make_orchestrator() + # Register a participant so the tracker recognizes it. + orch.agents = {"Coordinator": object(), "Chief Architect": object()} + + for _ in range(3): + orch._track_participant_completion("Chief Architect") + + assert orch._forced_termination_requested is True, ( + "Three consecutive completions of the same participant must " + "trigger the participant-streak loop breaker; otherwise the " + "Chief-Architect-only loop observed in production (with the " + "Coordinator invisible to our streaming loop in 1.3.0) can " + "never be detected and the workflow runs until the framework's " + "own max_rounds ceiling fires." + ) + assert orch._forced_termination_type == "hard_loop" + assert "Chief Architect" in (orch._forced_termination_reason or "") + assert "3 consecutive" in (orch._forced_termination_reason or "") + + asyncio.run(_run()) + + +def test_participant_completion_streak_resets_on_different_participant(): + """If a different participant runs in between, the same-participant + streak counter resets. This prevents false-positive loop detection + when participants alternate normally. + """ + + async def _run(): + orch = _make_orchestrator() + orch.agents = { + "Coordinator": object(), + "Chief Architect": object(), + "AKS Expert": object(), + } + + orch._track_participant_completion("Chief Architect") + orch._track_participant_completion("Chief Architect") + # A different participant runs -> streak resets. + orch._track_participant_completion("AKS Expert") + orch._track_participant_completion("Chief Architect") + orch._track_participant_completion("Chief Architect") # streak=2 only + + assert orch._forced_termination_requested is False, ( + "Alternating participants must not trigger the loop breaker; " + "the streak should reset whenever a different participant runs." + ) + assert orch._participant_completion_streak == 2 + assert orch._last_completed_participant == "Chief Architect" + + asyncio.run(_run()) + + +def test_participant_completions_total_enforces_max_rounds_under_alternation(): + """``max_rounds`` must be enforced from the per-participant total count + (which grows on EVERY completion) - not from ``len(agent_responses)`` + (which only grows on agent switch in ``_start_agent_if_needed`` and + therefore can never reach ``max_rounds`` during a same-agent loop). + + This test exercises the alternation case where the streak detector + never fires, ensuring the round-budget guard still halts the workflow. + """ + + async def _run(): + orch = GroupChatOrchestrator( + name="t", + process_id="p1", + participants={ + "Coordinator": object(), + "A": object(), + "B": object(), + }, + memory_client=None, + coordinator_name="Coordinator", + max_rounds=4, + result_output_format=None, + ) + + # Alternate A and B to keep the streak below threshold. + orch._track_participant_completion("A") + orch._track_participant_completion("B") + orch._track_participant_completion("A") + # Streak detector hasn't fired yet (max streak = 1 because of perfect + # alternation). The 4th turn must trip the max_rounds budget. + assert orch._forced_termination_requested is False + orch._track_participant_completion("B") + + assert orch._forced_termination_requested is True + assert orch._forced_termination_type == "hard_timeout" + assert "max_rounds=4" in (orch._forced_termination_reason or "") + + asyncio.run(_run()) From 03387c45df1fd5f0cc6cac68c0e06ecf9d28556d Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Sat, 13 Jun 2026 15:52:36 +0530 Subject: [PATCH 34/50] Revert "fix(groupchat): detect participant loops via executor_completed events" This reverts commit 7a0212fdaa6594dc8383327e95cf9661c7e200f5. --- .../agent_framework/groupchat_orchestrator.py | 163 +----------------- ...test_groupchat_orchestrator_termination.py | 110 ------------ 2 files changed, 9 insertions(+), 264 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 3185f6eb..94079d93 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -313,33 +313,6 @@ def __init__( # Snapshot of progress_counter at the time we last saw _last_coordinator_selection. self._last_coordinator_selection_progress: int = 0 - # Per-participant turn tracking driven by ``WorkflowEvent.executor_completed``. - # - # In agent-framework 1.3.0 the GroupChat orchestrator agent (the - # Coordinator) is invoked directly inside the framework's internal - # ``_invoke_agent_helper`` (see - # ``agent_framework_orchestrations/_group_chat.py:484``). It is NOT - # wrapped in an ``AgentExecutor`` and therefore never surfaces as a - # workflow event - which makes the Coordinator-JSON-based loop - # detection in ``_complete_agent_response`` permanently dead in 1.3.0. - # - # The only observable "the conversation is moving" pulse we have is - # ``executor_completed`` events for the *participants* (which DO go - # through ``AgentExecutor``). We track: - # - the most recently completed participant, - # - the streak of consecutive completions of that participant, - # - the total number of participant turns, - # and use these for two safety nets in the streaming loop: - # * 3+ consecutive same-participant turns => hard_loop termination - # * total turns >= ``max_rounds`` => hard_timeout termination - # (independent of ``len(self.agent_responses)`` which only grows on - # agent switch and so cannot reach ``max_rounds`` during a same- - # participant loop). - self._participant_completions_total: int = 0 - self._last_completed_participant: str | None = None - self._participant_completion_streak: int = 0 - self._participant_consecutive_loop_threshold: int = 3 - def _request_forced_termination( self, *, reason: str, termination_type: str ) -> None: @@ -570,15 +543,6 @@ async def run_stream( termination_type="hard_timeout", ) - # Honor any pending termination request at the *top* of each - # iteration so that branches which set the flags (timeout, - # participant loop detection, Coordinator finish=true) take - # effect immediately on the next event - rather than being - # gated on the next ``output`` event arriving (which during a - # slow loop can be many seconds away). - if self._forced_termination_requested or self._termination_requested: - break - # In agent-framework 1.3.0, ``workflow.run(stream=True)`` yields # only ``WorkflowEvent`` instances; ``AgentResponseUpdate`` is # wrapped inside ``WorkflowEvent.data`` for ``type=="output"`` @@ -588,46 +552,7 @@ async def run_stream( # ``WorkflowEvent.type`` and inspect ``event.data`` / # ``event.executor_id`` to route per-participant streaming # chunks vs the orchestrator's final output. - if not isinstance(event, WorkflowEvent): - continue - - # Participant turn completion. Used for loop / max_rounds - # safety nets that work even when the Coordinator is - # invisible to the streaming loop (which it is in 1.3.0 - - # the Coordinator runs inside the framework's internal - # ``_invoke_agent_helper`` and never surfaces as an executor - # event). See ``_track_participant_completion`` for details. - if event.type == "executor_completed": - src_executor = self._normalize_executor_id( - event.executor_id or "" - ) - if ( - src_executor in self.agents - and src_executor != self.coordinator_name - and src_executor != self.get_result_generator_name() - ): - # Flush this participant's streaming buffer into a - # discrete per-turn ``AgentResponse`` before we track - # the completion. Without this, when the framework's - # Coordinator picks the same participant back-to-back - # (the loop pattern we're trying to detect), - # ``_start_agent_if_needed`` sees no agent switch on - # the NEXT turn's chunks and the buffer would grow - # across turns - producing one merged response rather - # than one response per turn. - if ( - self._last_executor_id == src_executor - and self._current_agent_response - ): - await self._complete_agent_response( - src_executor, on_agent_response - ) - self._current_agent_response = [] - self._last_executor_id = None - self._track_participant_completion(src_executor) - continue - - if event.type != "output": + if not isinstance(event, WorkflowEvent) or event.type != "output": continue data = event.data @@ -648,12 +573,7 @@ async def run_stream( callback=on_agent_response, ) - # Secondary max_rounds safety net based on agent switches. - # The primary check lives in ``_track_participant_completion`` - # (driven by ``executor_completed`` events) and works even - # when the same agent runs back-to-back. This switch-based - # check is kept as defense-in-depth for sessions with - # normal alternation. + # Enforce max rounds as a safety guard. if self.max_rounds and len(self.agent_responses) >= self.max_rounds: self._request_forced_termination( reason=( @@ -662,9 +582,13 @@ async def run_stream( termination_type="hard_timeout", ) - # Termination flags are honored at the top of the next - # iteration so any branch can request termination - # uniformly without duplicating break logic here. + if self._forced_termination_requested: + break + + # If the Coordinator requested finish=true, stop immediately. + if self._termination_requested: + break + continue # Final orchestrator output: complete any buffered agent @@ -853,75 +777,6 @@ def _normalize_executor_id(self, executor_id: str) -> str: """ return executor_id.split(":")[-1] - def _track_participant_completion(self, src_executor: str) -> None: - """Track a participant turn completion for loop / max_rounds detection. - - Called from the streaming loop on every ``WorkflowEvent.type == - "executor_completed"`` event whose ``executor_id`` matches one of our - registered non-Coordinator, non-ResultGenerator participants. - - Why this exists (agent-framework 1.3.0 design constraint): - The framework's ``GroupChatBuilder.orchestrator_agent`` (our - Coordinator) is invoked directly via ``self._agent.run(...)`` - inside ``agent_framework_orchestrations/_group_chat.py:484``. It - is NOT wrapped in an ``AgentExecutor`` and therefore never - surfaces as a workflow event. Our existing Coordinator-JSON-based - loop detector in ``_complete_agent_response`` (lines ~1118-1181) - is consequently permanently dead in 1.3.0. We need an independent - loop signal that does NOT rely on Coordinator visibility. - - Two safety nets enforced here: - - 1. Same-participant streak (``_participant_consecutive_loop_threshold``, - default 3): if the Coordinator keeps selecting the same participant - (e.g., the Chief Architect latched on producing an Evidence Pack - that never satisfies the next reviewer), 3+ consecutive completions - of the same participant force-terminate with ``hard_loop``. - - 2. Total round budget: each participant turn counts as one round. - Once total completions reach ``self.max_rounds`` the workflow - force-terminates with ``hard_timeout``. This is independent of - ``len(self.agent_responses)`` (which only grows on agent switch - via ``_start_agent_if_needed`` and therefore cannot reach - ``max_rounds`` during a same-participant loop). - """ - if src_executor == self._last_completed_participant: - self._participant_completion_streak += 1 - else: - self._last_completed_participant = src_executor - self._participant_completion_streak = 1 - self._participant_completions_total += 1 - - if ( - self._participant_completion_streak - >= self._participant_consecutive_loop_threshold - ): - self._request_forced_termination( - reason=( - f"Loop detected: participant '{src_executor}' completed " - f"{self._participant_completion_streak} consecutive turns " - "with no other participant in between (Coordinator is " - "stuck on the same selection; in agent-framework 1.3.0 " - "the Coordinator runs inside the framework and is " - "invisible to the streaming loop, so we infer this from " - "executor_completed events)" - ), - termination_type="hard_loop", - ) - return - - if ( - self.max_rounds - and self._participant_completions_total >= self.max_rounds - ): - self._request_forced_termination( - reason=( - f"Workflow exceeded max_rounds={self.max_rounds} " - "participant turns; terminating to avoid infinite loop" - ), - termination_type="hard_timeout", - ) - async def _start_agent_if_needed( self, agent_name: str, diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py index 2bd88d65..73eb7e6b 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py @@ -293,113 +293,3 @@ async def _run(): ) asyncio.run(_run()) - - -def test_participant_completion_streak_triggers_forced_termination(): - """In agent-framework 1.3.0 the GroupChat orchestrator agent (Coordinator) - is invoked directly inside the framework's ``_invoke_agent_helper`` and - is NOT wrapped in an ``AgentExecutor``, so it never surfaces as a - workflow event. The Coordinator-JSON loop detector in - ``_complete_agent_response`` is therefore permanently dead in 1.3.0. - - The only observable loop signal we have is consecutive - ``executor_completed`` events for the same participant. After - ``_participant_consecutive_loop_threshold`` (default 3) same-participant - completions, the orchestrator must force-terminate with ``hard_loop`` - so the workflow halts cleanly instead of running until the framework's - own max_rounds ceiling (which at default 100 is ~17 min). - """ - - async def _run(): - orch = _make_orchestrator() - # Register a participant so the tracker recognizes it. - orch.agents = {"Coordinator": object(), "Chief Architect": object()} - - for _ in range(3): - orch._track_participant_completion("Chief Architect") - - assert orch._forced_termination_requested is True, ( - "Three consecutive completions of the same participant must " - "trigger the participant-streak loop breaker; otherwise the " - "Chief-Architect-only loop observed in production (with the " - "Coordinator invisible to our streaming loop in 1.3.0) can " - "never be detected and the workflow runs until the framework's " - "own max_rounds ceiling fires." - ) - assert orch._forced_termination_type == "hard_loop" - assert "Chief Architect" in (orch._forced_termination_reason or "") - assert "3 consecutive" in (orch._forced_termination_reason or "") - - asyncio.run(_run()) - - -def test_participant_completion_streak_resets_on_different_participant(): - """If a different participant runs in between, the same-participant - streak counter resets. This prevents false-positive loop detection - when participants alternate normally. - """ - - async def _run(): - orch = _make_orchestrator() - orch.agents = { - "Coordinator": object(), - "Chief Architect": object(), - "AKS Expert": object(), - } - - orch._track_participant_completion("Chief Architect") - orch._track_participant_completion("Chief Architect") - # A different participant runs -> streak resets. - orch._track_participant_completion("AKS Expert") - orch._track_participant_completion("Chief Architect") - orch._track_participant_completion("Chief Architect") # streak=2 only - - assert orch._forced_termination_requested is False, ( - "Alternating participants must not trigger the loop breaker; " - "the streak should reset whenever a different participant runs." - ) - assert orch._participant_completion_streak == 2 - assert orch._last_completed_participant == "Chief Architect" - - asyncio.run(_run()) - - -def test_participant_completions_total_enforces_max_rounds_under_alternation(): - """``max_rounds`` must be enforced from the per-participant total count - (which grows on EVERY completion) - not from ``len(agent_responses)`` - (which only grows on agent switch in ``_start_agent_if_needed`` and - therefore can never reach ``max_rounds`` during a same-agent loop). - - This test exercises the alternation case where the streak detector - never fires, ensuring the round-budget guard still halts the workflow. - """ - - async def _run(): - orch = GroupChatOrchestrator( - name="t", - process_id="p1", - participants={ - "Coordinator": object(), - "A": object(), - "B": object(), - }, - memory_client=None, - coordinator_name="Coordinator", - max_rounds=4, - result_output_format=None, - ) - - # Alternate A and B to keep the streak below threshold. - orch._track_participant_completion("A") - orch._track_participant_completion("B") - orch._track_participant_completion("A") - # Streak detector hasn't fired yet (max streak = 1 because of perfect - # alternation). The 4th turn must trip the max_rounds budget. - assert orch._forced_termination_requested is False - orch._track_participant_completion("B") - - assert orch._forced_termination_requested is True - assert orch._forced_termination_type == "hard_timeout" - assert "max_rounds=4" in (orch._forced_termination_reason or "") - - asyncio.run(_run()) From 14d52f26638030c90ef2a9640dd7958867a9840e Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Sat, 13 Jun 2026 16:09:27 +0530 Subject: [PATCH 35/50] chore(logging): suppress harmless empty-message-cache warning and drop dead code * Add a narrow logging.Filter on agent_framework._workflows._agent_executor that drops only the 'Running agent with empty message cache' message. This warning fires by design in GroupChat orchestration when the orchestrator routes back to the same speaker (broadcast cache is empty because _broadcast_messages_to_participants excludes the source executor). The framework's parent client prepends system instructions before the LLM call, so the API request still has content. Other warnings/errors from the same logger remain visible. * Remove three lines of commented-out duplicate callback invocation in groupchat_orchestrator._complete_agent_response. The live callback handler is in the block directly above; the commented block was refactor debris. No behavioural change. All 833 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 4 --- src/processor/src/utils/logging_utils.py | 30 +++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 94079d93..90a0f914 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -1250,10 +1250,6 @@ async def _complete_agent_response( "on_agent_response callback failed (agent=%s)", agent_name ) - # # Invoke callback - # if callback: - # await callback(response) - async def _build_groupchat(self) -> Workflow: """Build the GroupChat Orchestrator workflow""" coordinator = self.agents[self.coordinator_name] diff --git a/src/processor/src/utils/logging_utils.py b/src/processor/src/utils/logging_utils.py index 29da2226..568ed950 100644 --- a/src/processor/src/utils/logging_utils.py +++ b/src/processor/src/utils/logging_utils.py @@ -22,6 +22,29 @@ from azure.core.exceptions import HttpResponseError +class _EmptyMessageCacheFilter(logging.Filter): + """Suppress the harmless ``empty message cache`` warning emitted by + ``agent_framework._workflows._agent_executor``. + + This warning fires by design in GroupChat orchestration when the orchestrator + routes back to the same speaker (its broadcast cache is empty because + ``_broadcast_messages_to_participants`` excludes the source executor). The + framework's parent client prepends the agent's system instructions before + calling the LLM, so the API call still has content. The warning is pure noise. + + The filter is intentionally narrow: it matches only the exact message and + leaves every other warning/error from the same logger visible. + """ + + _MARKER = "Running agent with empty message cache" + + def filter(self, record: logging.LogRecord) -> bool: # noqa: D401 + try: + return self._MARKER not in record.getMessage() + except Exception: + return True + + def configure_application_logging(debug_mode: bool = False): """ Comprehensive logging configuration with third-party suppression. @@ -120,6 +143,13 @@ def configure_application_logging(debug_mode: bool = False): for logger_name in always_warning_loggers: logging.getLogger(logger_name).setLevel(logging.WARNING) + # Suppress only the harmless "Running agent with empty message cache" warning + # emitted by agent_framework's GroupChat orchestration. Real warnings/errors + # from the same logger are still surfaced. + _executor_logger = logging.getLogger("agent_framework._workflows._agent_executor") + if not any(isinstance(f, _EmptyMessageCacheFilter) for f in _executor_logger.filters): + _executor_logger.addFilter(_EmptyMessageCacheFilter()) + # Set environment variables to suppress verbose output at the source os.environ.setdefault("HTTPX_LOG_LEVEL", "WARNING") os.environ.setdefault("AZURE_CORE_ENABLE_HTTP_LOGGER", "false") From 25aeea5780892da5ae8c364220b9696280243d93 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Tue, 16 Jun 2026 15:05:01 +0530 Subject: [PATCH 36/50] ci: replace deprecated --exclude-mail with --exclude ^mailto: in lychee action The --exclude-mail flag was removed in lychee v0.23.0. Replace with equivalent --exclude pattern to fix CI failures on PRs touching .md files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/broken-links-checker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/broken-links-checker.yml b/.github/workflows/broken-links-checker.yml index af100332..f791df64 100644 --- a/.github/workflows/broken-links-checker.yml +++ b/.github/workflows/broken-links-checker.yml @@ -37,7 +37,7 @@ jobs: uses: lycheeverse/lychee-action@v2.8.0 with: args: > - --verbose --exclude-mail --no-progress --exclude ^https?:// + --verbose --no-progress --exclude ^https?:// --exclude ^mailto: ${{ steps.changed-markdown-files.outputs.all_changed_files }} failIfEmpty: false env: @@ -50,7 +50,7 @@ jobs: uses: lycheeverse/lychee-action@v2.8.0 with: args: > - --verbose --exclude-mail --no-progress --exclude ^https?:// + --verbose --no-progress --exclude ^https?:// --exclude ^mailto: '**/*.md' failIfEmpty: false env: From db7c8379d96d9e85a7448aa2193bebe98e5e6d9b Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Tue, 16 Jun 2026 18:49:39 +0530 Subject: [PATCH 37/50] fix: reconstruct conversation from agent_responses on early termination When the Coordinator signals finish=true, the streaming loop breaks before the framework's final output event (carrying the conversation) arrives. This left the conversation empty, causing ResultGenerator to produce 'nothing to summarize' and failing the step despite actual work being done. Two fixes: 1. After the streaming loop, if conversation is empty but agent_responses exist, reconstruct the conversation from collected agent responses. 2. Sign-off validation now uses agent_responses when self._conversation is still empty, fixing vacuous validation during early termination. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 90a0f914..4143aa10 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -606,6 +606,21 @@ async def run_stream( conversation = getattr(data, "conversation", []) self._conversation = conversation # Update instance variable + # Fallback: when the streaming loop breaks early (e.g. Coordinator + # finish=true termination), the framework's final output event + # carrying the conversation is never received and ``conversation`` + # stays empty. Reconstruct a best-effort conversation from the + # agent_responses we collected during streaming so the + # ResultGenerator still has context to work with. + if not conversation and self.agent_responses: + logger.warning( + "[CONVERSATION] Final output event missed (early termination); " + "reconstructing conversation from %d agent_responses.", + len(self.agent_responses), + ) + conversation = self._reconstruct_conversation_from_responses() + self._conversation = conversation + # Backfill tool usage from the final conversation (more reliable than streaming updates) # AgentResponseUpdate may stream text only; tool calls are represented as FunctionCallContent # items inside Message.contents. @@ -1200,7 +1215,16 @@ async def _complete_agent_response( if coordinator_signaled_stop: # Only enforce PASS sign-offs when Coordinator claims success completion. if instruction == "complete": - is_valid, reason = self._validate_sign_offs(self._conversation) + # Use the reconstructed conversation from agent_responses + # because self._conversation (populated only by the final + # output event) is still empty at this point in the + # streaming loop. + sign_off_conversation = ( + self._conversation + if self._conversation + else self._reconstruct_conversation_from_responses() + ) + is_valid, reason = self._validate_sign_offs(sign_off_conversation) if not is_valid: logger.warning( "Termination rejected for success completion: %s. Workflow continues.", @@ -1473,6 +1497,32 @@ def _build_result_generator_conversation( selected.reverse() return selected + def _reconstruct_conversation_from_responses(self) -> list[Message]: + """Reconstruct a conversation from agent responses collected during streaming. + + When the streaming loop breaks early (e.g., Coordinator termination), + the framework's final output event may not arrive, leaving the + conversation empty. This method builds a best-effort conversation + from ``self.agent_responses`` so the ResultGenerator still has the + full agent discussion to summarize. + """ + messages: list[Message] = [] + for resp in self.agent_responses: + if not resp.message: + continue + messages.append( + Message( + role="assistant", + contents=[resp.message], + author_name=resp.agent_name, + ) + ) + logger.info( + "[CONVERSATION] Reconstructed %d messages from agent_responses.", + len(messages), + ) + return messages + def get_tool_usage_summary(self) -> dict[str, Any]: """Get summary of tool usage across all agents""" total_calls = sum(len(calls) for calls in self.agent_tool_usage.values()) From d0397f74346c6fb957605283339a284ed8199b35 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Wed, 17 Jun 2026 10:29:02 +0530 Subject: [PATCH 38/50] fix: use correct Message attributes in _validate_sign_offs (text/author_name) _validate_sign_offs was using msg.content and msg.source which do not exist on agent-framework 1.3.0 Message objects, causing runtime crashes. Updated to use msg.text and msg.author_name. Updated corresponding test mocks to use the new API attributes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../libs/agent_framework/groupchat_orchestrator.py | 4 ++-- .../test_groupchat_orchestrator_internals.py | 14 +++++++------- .../test_groupchat_orchestrator_termination.py | 12 +++++++----- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 4143aa10..792b3be7 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -398,8 +398,8 @@ def _validate_sign_offs(self, conversation: list[Message]) -> tuple[bool, str]: # Search for sign-off patterns in messages for msg in recent_messages: - content = str(msg.content).upper() - agent_name = msg.source if hasattr(msg, "source") else None + content = (msg.text or str(msg.contents)).upper() + agent_name = getattr(msg, "author_name", None) or getattr(msg, "source", None) if not agent_name or agent_name == self.coordinator_name: continue diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index 5a1121c8..38c9f0c5 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -219,29 +219,29 @@ class TestValidateSignOffs: def test_all_pass(self): orch = _make_orch() msgs = [ - _Msg(source="A", content="SIGN-OFF: PASS"), - _Msg(source="B", content="SIGN-OFF:PASS"), + _Msg(author_name="A", text="SIGN-OFF: PASS"), + _Msg(author_name="B", text="SIGN-OFF:PASS"), ] ok, reason = orch._validate_sign_offs(msgs) assert ok is True def test_pending_blocks(self): orch = _make_orch() - msgs = [_Msg(source="A", content="SIGN-OFF: PENDING")] + msgs = [_Msg(author_name="A", text="SIGN-OFF: PENDING")] ok, reason = orch._validate_sign_offs(msgs) assert ok is False assert "PENDING" in reason def test_fail_blocks(self): orch = _make_orch() - msgs = [_Msg(source="A", content="SIGN-OFF: FAIL")] + msgs = [_Msg(author_name="A", text="SIGN-OFF: FAIL")] ok, reason = orch._validate_sign_offs(msgs) assert ok is False assert "FAIL" in reason def test_missing_blocks(self): orch = _make_orch() - msgs = [_Msg(source="A", content="some text without signoff")] + msgs = [_Msg(author_name="A", text="some text without signoff")] ok, reason = orch._validate_sign_offs(msgs) assert ok is False assert "missing" in reason @@ -249,8 +249,8 @@ def test_missing_blocks(self): def test_excludes_coordinator_and_resultgenerator(self): orch = _make_orch() msgs = [ - _Msg(source="Coordinator", content="ignored"), - _Msg(source="ResultGenerator", content="ignored"), + _Msg(author_name="Coordinator", text="ignored"), + _Msg(author_name="ResultGenerator", text="ignored"), ] ok, _ = orch._validate_sign_offs(msgs) assert ok is True diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py index 73eb7e6b..0117b791 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py @@ -13,8 +13,10 @@ @dataclass class _Msg: - source: str - content: str + author_name: str = "" + text: str = "" + contents: object = None + role: object = None def _make_orchestrator() -> GroupChatOrchestrator: @@ -34,8 +36,8 @@ async def _run(): # Everyone who participated signed off PASS. orch._conversation = [ - _Msg(source="AKS Expert", content="SIGN-OFF: PASS"), - _Msg(source="Chief Architect", content="SIGN-OFF: PASS"), + _Msg(author_name="AKS Expert", text="SIGN-OFF: PASS"), + _Msg(author_name="Chief Architect", text="SIGN-OFF: PASS"), ] orch._current_agent_start_time = datetime.now() @@ -65,7 +67,7 @@ async def _run(): # Agent participated but never produced a SIGN-OFF. orch._conversation = [ - _Msg(source="AKS Expert", content="Reviewed; looks good."), + _Msg(author_name="AKS Expert", text="Reviewed; looks good."), ] orch._current_agent_start_time = datetime.now() From b9979a7fc64ebd52c7b7d987167bbc07bea67dc3 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Wed, 17 Jun 2026 12:12:59 +0530 Subject: [PATCH 39/50] fix: add loop detection for same-agent back-to-back invocations in 1.3.0 In agent-framework 1.3.0, GroupChatBuilder makes the Coordinator internal to AgentBasedGroupChatOrchestrator - its responses never appear in the streaming loop, making our coordinator-based loop detection dead code. Additionally, _start_agent_if_needed silently returned when the same agent was selected consecutively, preventing max_rounds from counting. Fixes: - Use response_id from AgentResponseUpdate to detect new invocations of the same agent (even when executor_id hasn't changed) - Add participant-side loop detection: if same agent runs 5+ consecutive times, force termination with descriptive message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 792b3be7..9e7c422b 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -265,6 +265,7 @@ def __init__( # Streaming response buffer self._last_executor_id: str | None = None + self._last_response_id: str | None = None self._current_agent_response: list[str] = [] self._current_agent_start_time: datetime | None = None @@ -781,7 +782,14 @@ async def _handle_agent_update( agent_name = author_name or self._normalize_executor_id( getattr(event, "agent_id", None) or "" ) - await self._start_agent_if_needed(agent_name, stream_callback, callback) + # Detect new invocations of the same agent via response_id change. + # In 1.3.0 with GroupChatBuilder, the Coordinator is internal — we only + # see participant events. When the same participant runs back-to-back, + # the executor_id is identical but response_id differs per invocation. + response_id = getattr(event, "response_id", None) + await self._start_agent_if_needed( + agent_name, stream_callback, callback, response_id=response_id + ) self._append_text_chunk(event) await self._process_tool_calls(event, agent_name, stream_callback) @@ -797,11 +805,29 @@ async def _start_agent_if_needed( agent_name: str, stream_callback: AgentResponseStreamCallback | None, callback: AgentResponseCallback | None, + response_id: str | None = None, ) -> None: - """Handle agent switches and emit a message-start stream event.""" - if agent_name == self._last_executor_id: + """Handle agent switches and emit a message-start stream event. + + In agent-framework 1.3.0 with GroupChatBuilder, the Coordinator is + internal — our streaming loop only sees participant events. When the + same participant is selected back-to-back, the executor_id is identical + but ``response_id`` differs per invocation. We use this to detect new + turns of the same agent. + """ + # Detect same-agent new invocation via response_id change. + is_new_response = ( + response_id is not None + and self._last_response_id is not None + and response_id != self._last_response_id + ) + if agent_name == self._last_executor_id and not is_new_response: + # Same agent, same response — just accumulating streaming chunks. return + if response_id is not None: + self._last_response_id = response_id + # Complete and save previous agent's response if self._last_executor_id and self._current_agent_response: await self._complete_agent_response(self._last_executor_id, callback) @@ -1106,6 +1132,32 @@ async def _complete_agent_response( self.agent_responses.append(response) + # Participant-side loop detection. In 1.3.0 with GroupChatBuilder, the + # Coordinator is internal (its responses never appear in our streaming + # loop), so the Coordinator-based loop detection below (streak >= 3) is + # dead code. Instead, detect loops by checking if the last N responses + # are all from the same non-Coordinator agent. + if agent_name != self.coordinator_name: + consecutive_same = 0 + for r in reversed(self.agent_responses): + if r.agent_name == agent_name: + consecutive_same += 1 + else: + break + if consecutive_same >= 5: + logger.warning( + "[LOOP] Same agent '%s' ran %d consecutive times; forcing termination.", + agent_name, + consecutive_same, + ) + self._request_forced_termination( + reason=( + f"Loop detected: '{agent_name}' ran {consecutive_same} " + f"consecutive times without any other agent participating" + ), + termination_type="hard_timeout", + ) + # Mark progress on any non-Coordinator completion. This is used to ensure loop # detection only triggers when the Coordinator is repeating itself *and* the # rest of the conversation is not advancing. From 4a8ee72707d43711a25603bd246f381508a3073d Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Wed, 17 Jun 2026 12:31:41 +0530 Subject: [PATCH 40/50] fix: address Copilot review - empty name handling and type annotation - _sanitize_author_name returns None for empty string (avoids invalid OpenAI name field that doesn't match ^[^\s<|\\/>]+$ regex) - Fix create_agents return type: dict[str, Agent] not list[Agent] - Update test assertion to match new empty-string behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/libs/agent_framework/azure_openai_response_retry.py | 4 +++- src/processor/src/libs/base/orchestrator_base.py | 2 +- .../agent_framework/test_azure_openai_response_retry_utils.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index b597afac..d0b0caab 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -284,8 +284,10 @@ def _sanitize_author_name(name: Any) -> Any: empty (e.g. name was all whitespace), returns ``None`` so the field can be dropped entirely. """ - if not isinstance(name, str) or not name: + if not isinstance(name, str): return name + if not name: + return None if not _OPENAI_NAME_INVALID_CHARS.search(name): return name sanitized = _OPENAI_NAME_INVALID_CHARS.sub("_", name).strip("_") diff --git a/src/processor/src/libs/base/orchestrator_base.py b/src/processor/src/libs/base/orchestrator_base.py index f21473e1..0eda9944 100644 --- a/src/processor/src/libs/base/orchestrator_base.py +++ b/src/processor/src/libs/base/orchestrator_base.py @@ -145,7 +145,7 @@ async def prepare_agent_infos(self) -> list[AgentInfo]: async def create_agents( self, agent_infos: list[AgentInfo], process_id: str - ) -> list[Agent]: + ) -> dict[str, Agent]: agents = dict[str, Agent]() agent_client = await self.get_client(thread_id=process_id) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_azure_openai_response_retry_utils.py b/src/processor/src/tests/unit/libs/agent_framework/test_azure_openai_response_retry_utils.py index 7236d192..16a5416b 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_azure_openai_response_retry_utils.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_azure_openai_response_retry_utils.py @@ -111,7 +111,7 @@ def test_sanitize_author_name_replaces_whitespace_and_specials() -> None: def test_sanitize_author_name_handles_edge_cases() -> None: assert _sanitize_author_name(None) is None - assert _sanitize_author_name("") == "" + assert _sanitize_author_name("") is None assert _sanitize_author_name(123) == 123 # All-invalid input collapses to empty -> None (so callers drop the field). assert _sanitize_author_name(" ") is None From 488a290ce92d6e2b0d226795432c18e6150bf41c Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Wed, 17 Jun 2026 13:16:02 +0530 Subject: [PATCH 41/50] fix: use selection_func to keep Coordinator visible in streaming loop In agent-framework 1.3.0, orchestrator_agent= wraps the Coordinator inside AgentBasedGroupChatOrchestrator, making its responses invisible to our streaming loop. This disabled loop detection (streak >= 3), finish signal extraction, and sign-off validation - all dead code. The old set_manager() API no longer exists in 1.3.0. Instead, use selection_func= with the Coordinator as a regular participant. The selection function alternates Coordinator <-> participants by parsing the Coordinator's JSON to find the next speaker. This restores the old behavior where: - Coordinator responses are visible in the stream - Loop detection fires at 3 repeated same-agent selections - finish=true is parsed and acted on immediately - Sign-off validation runs before termination The participant-side loop detection (5 consecutive) is kept as a safety net for edge cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 128 ++++++++++++------ .../test_groupchat_orchestrator_internals.py | 7 +- 2 files changed, 93 insertions(+), 42 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 9e7c422b..2b0f069a 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -31,7 +31,7 @@ Workflow, WorkflowEvent, ) -from agent_framework.orchestrations import GroupChatBuilder +from agent_framework.orchestrations import GroupChatBuilder, GroupChatState from mem0 import AsyncMemory from pydantic import AliasChoices, BaseModel, Field, ValidationError @@ -47,17 +47,13 @@ class ManagerSelectionResponse(BaseModel): """Coordinator selection payload parsed from JSON output. The Coordinator prompt instructs the model to emit fields named - ``selected_participant`` / ``instruction`` / ``finish``. However, the - underlying ``agent_framework_orchestrations.GroupChatBuilder`` forces the - Coordinator's response_format to ``AgentOrchestrationOutput`` (strict - schema with fields ``next_speaker`` / ``reason`` / ``terminate``). With - strict structured output, the model always emits the framework's field - names regardless of the prompt. - - We use Pydantic ``AliasChoices`` so this model accepts BOTH naming - conventions transparently. Without these aliases, parsing silently - succeeds (``extra=allow``) but every field ends up ``None``, disabling - loop detection and Coordinator-driven termination. + ``selected_participant`` / ``instruction`` / ``finish``. We use + ``selection_func`` (not ``orchestrator_agent``) so the Coordinator + runs as a regular participant and the framework does NOT override + its ``response_format``. The model should emit our prompt field + names, but we keep ``AliasChoices`` for robustness in case the + framework's ``AgentOrchestrationOutput`` names + (``next_speaker`` / ``reason`` / ``terminate``) leak through. """ selected_participant: str | None = Field( @@ -783,9 +779,9 @@ async def _handle_agent_update( getattr(event, "agent_id", None) or "" ) # Detect new invocations of the same agent via response_id change. - # In 1.3.0 with GroupChatBuilder, the Coordinator is internal — we only - # see participant events. When the same participant runs back-to-back, - # the executor_id is identical but response_id differs per invocation. + # When the same agent runs back-to-back (e.g. Coordinator selected + # twice on parse failure), the executor_id is identical but + # response_id differs per invocation. response_id = getattr(event, "response_id", None) await self._start_agent_if_needed( agent_name, stream_callback, callback, response_id=response_id @@ -809,11 +805,10 @@ async def _start_agent_if_needed( ) -> None: """Handle agent switches and emit a message-start stream event. - In agent-framework 1.3.0 with GroupChatBuilder, the Coordinator is - internal — our streaming loop only sees participant events. When the - same participant is selected back-to-back, the executor_id is identical - but ``response_id`` differs per invocation. We use this to detect new - turns of the same agent. + With ``selection_func`` the Coordinator IS a visible participant, + so we see both Coordinator and participant events. The response_id + detection is kept as a safety net for same-agent back-to-back + invocations (e.g. Coordinator → Coordinator on parse failure). """ # Detect same-agent new invocation via response_id change. is_new_response = ( @@ -1132,11 +1127,10 @@ async def _complete_agent_response( self.agent_responses.append(response) - # Participant-side loop detection. In 1.3.0 with GroupChatBuilder, the - # Coordinator is internal (its responses never appear in our streaming - # loop), so the Coordinator-based loop detection below (streak >= 3) is - # dead code. Instead, detect loops by checking if the last N responses - # are all from the same non-Coordinator agent. + # Participant-side loop detection (safety net). The Coordinator-based + # detection below (streak >= 3) is the primary guard. This catches + # edge cases where the Coordinator itself loops (e.g. selection_func + # returns Coordinator repeatedly on parse failure). if agent_name != self.coordinator_name: consecutive_same = 0 for r in reversed(self.agent_responses): @@ -1327,7 +1321,25 @@ async def _complete_agent_response( ) async def _build_groupchat(self) -> Workflow: - """Build the GroupChat Orchestrator workflow""" + """Build the GroupChat Orchestrator workflow. + + In agent-framework 1.3.0 the old ``GroupChatBuilder().set_manager()`` + API was replaced. Using ``orchestrator_agent=`` wraps the Coordinator + inside ``AgentBasedGroupChatOrchestrator``, making its responses + invisible to our streaming loop and disabling loop detection, finish + signal extraction, and sign-off validation. + + To restore the old behavior we use ``selection_func=`` with the + Coordinator included as a regular participant. The selection function + alternates between the Coordinator and participants: + + 1. After a participant responds → select Coordinator + 2. After the Coordinator responds → parse its JSON to find the + next participant (or fall back to Coordinator on parse failure) + + This keeps the Coordinator visible in the stream so all existing + detection/termination logic works unchanged. + """ coordinator = self.agents[self.coordinator_name] participants = [ agent @@ -1336,22 +1348,58 @@ async def _build_groupchat(self) -> Workflow: and name != self.get_result_generator_name() ] - # ``max_rounds`` is enforced at the framework level so the workflow - # halts cleanly even if our orchestrator-side guards miss an event - # shape. Without this, the framework's default behavior is "continue - # indefinitely" (see GroupChatBuilder docstring) until the workflow - # runner hits its own 100-iteration cap and raises - # ``RuntimeError("Runner did not converge after 100 iterations")``. - # - # ``intermediate_outputs=True`` surfaces each participant's - # ``yield_output(AgentResponseUpdate)`` call as a workflow ``output`` - # event. Without this, only the orchestrator's final yield reaches - # our streaming loop, which means per-agent loop detection, finish - # signal extraction, and streaming callbacks all silently no-op. + # Include Coordinator as a regular participant so its responses + # are visible in the streaming loop (unlike orchestrator_agent=). + all_participants = [coordinator] + participants + coordinator_name = self.coordinator_name + + def selection_func(state: GroupChatState) -> str: + """Alternate between Coordinator and participants. + + - First turn (no conversation): select Coordinator. + - After a participant: select Coordinator to evaluate. + - After the Coordinator: parse its JSON response and return + the selected participant name. + """ + if not state.conversation: + return coordinator_name + + last_msg = state.conversation[-1] + last_author = getattr(last_msg, "author_name", None) or "" + + if last_author != coordinator_name: + # A participant just spoke → route to Coordinator + return coordinator_name + + # Coordinator just spoke → parse its selection JSON + last_text = getattr(last_msg, "text", None) or "" + try: + # Find JSON payload in the Coordinator's response + start = last_text.find("{") + end = last_text.rfind("}") + 1 + if start >= 0 and end > start: + response_dict = json.loads(last_text[start:end]) + manager_response = ManagerSelectionResponse.model_validate( + response_dict + ) + selected = getattr(manager_response, "selected_participant", None) + if ( + isinstance(selected, str) + and selected + and selected.lower() != "none" + and selected in state.participants + ): + return selected + except Exception: + pass + + # Fallback: route back to Coordinator (will trigger loop detection) + return coordinator_name + return ( GroupChatBuilder( - orchestrator_agent=coordinator, - participants=participants, + participants=all_participants, + selection_func=selection_func, max_rounds=self.max_rounds, intermediate_outputs=True, ) diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index 38c9f0c5..6c9f8c5a 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -854,9 +854,12 @@ def test_build_groupchat_invokes_builder(self): assert wf == "wf" # Check constructor args ctor_kwargs = MockBuilder.call_args.kwargs - assert ctor_kwargs["orchestrator_agent"] == "coord" - # ResultGenerator excluded from participants + # Coordinator is included as a regular participant via selection_func + assert "selection_func" in ctor_kwargs + assert callable(ctor_kwargs["selection_func"]) + # All participants include Coordinator + Architect (not ResultGenerator) participants = ctor_kwargs["participants"] + assert "coord" in participants assert "arch" in participants assert "rg" not in participants From 325bf399a5d55f13297afd143c165fd4493caad9 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Wed, 17 Jun 2026 13:52:44 +0530 Subject: [PATCH 42/50] diag: add Coordinator response logging for loop detection debugging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../libs/agent_framework/groupchat_orchestrator.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 2b0f069a..da4f9e8b 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -1177,6 +1177,12 @@ async def _complete_agent_response( # NOTE: The underlying GroupChatBuilder does not automatically stop on finish, # so we enforce it here. if agent_name == self.coordinator_name: + logger.info( + "[COORDINATOR] Processing Coordinator response (len=%d, streak=%d): %s", + len(complete_message), + self._coordinator_selection_streak, + complete_message[:150].replace("\n", " "), + ) try: json_payload = self._extract_first_json_payload(complete_message) response_dict = json.loads(json_payload) @@ -1304,11 +1310,11 @@ async def _complete_agent_response( if isinstance(complete_message, str) else str(type(complete_message)) ) - logger.debug( - "Coordinator JSON parse failed; skipping loop detection for " - "this turn. Raw message preview: %r", + logger.warning( + "[COORDINATOR] JSON parse failed; skipping loop detection. " + "Raw message preview: %r; error: %s", preview, - exc_info=exc, + str(exc), ) # Invoke callback with complete response From c5c3a3450cfcb7b12f8c789303168589058bf51f Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Wed, 17 Jun 2026 15:05:49 +0530 Subject: [PATCH 43/50] fix: set ManagerSelectionResponse extra=forbid for Chat API structured outputs OpenAI Chat Completions API requires additionalProperties: false in response_format schemas. The Responses API was more lenient, but environments using the Chat client (AzureOpenAIChatClientWithRetry) enforce this strictly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/libs/agent_framework/groupchat_orchestrator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index da4f9e8b..862c3ac0 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -70,7 +70,7 @@ class ManagerSelectionResponse(BaseModel): ) final_message: str | None = None - model_config = {"extra": "allow", "populate_by_name": True} + model_config = {"extra": "forbid", "populate_by_name": True} @dataclass From 89ec3fce98b2a393d56652eece6a1259f84bd14e Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Wed, 17 Jun 2026 15:25:55 +0530 Subject: [PATCH 44/50] fix: prevent infinite loop when sign-off validation rejects finish When Coordinator repeatedly says finish=true but sign-off validation fails, the selection_func routed back to Coordinator creating a 100-round loop. Fix: track consecutive finish rejections and force-terminate after 5 attempts. Also reset counter when Coordinator routes to a participant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 862c3ac0..72ab72ec 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -310,6 +310,12 @@ def __init__( # Snapshot of progress_counter at the time we last saw _last_coordinator_selection. self._last_coordinator_selection_progress: int = 0 + # Track consecutive finish rejections (sign-off validation failures). + # When the Coordinator keeps saying finish=true but sign-offs are + # incomplete, prevent infinite loops by force-terminating after a threshold. + self._finish_rejection_count: int = 0 + self._max_finish_rejections: int = 5 + def _request_forced_termination( self, *, reason: str, termination_type: str ) -> None: @@ -1278,10 +1284,26 @@ async def _complete_agent_response( ) is_valid, reason = self._validate_sign_offs(sign_off_conversation) if not is_valid: + self._finish_rejection_count += 1 logger.warning( - "Termination rejected for success completion: %s. Workflow continues.", + "Termination rejected for success completion (%d/%d): %s. Workflow continues.", + self._finish_rejection_count, + self._max_finish_rejections, reason, ) + if self._finish_rejection_count >= self._max_finish_rejections: + logger.warning( + "Max finish rejections reached (%d). Force-terminating.", + self._finish_rejection_count, + ) + self._request_forced_termination( + reason=( + f"Coordinator attempted finish {self._finish_rejection_count} times " + f"but sign-off validation kept failing: {reason}" + ), + termination_type="hard_timeout", + ) + return # Do NOT set _termination_requested. return @@ -1297,6 +1319,9 @@ async def _complete_agent_response( and selected and selected.lower() != "none" ): + # Non-finish routing: reset finish rejection counter since + # Coordinator is doing productive work (selecting participants). + self._finish_rejection_count = 0 # Record invocation time for non-termination coordinator selections self._agent_invoked_at[selected] = completed_at except Exception as exc: @@ -1396,6 +1421,12 @@ def selection_func(state: GroupChatState) -> str: and selected in state.participants ): return selected + + # Coordinator said finish=true with no valid participant. + # Route back to Coordinator to let the streaming loop's + # termination/force-termination logic handle it. + if manager_response.finish is True: + return coordinator_name except Exception: pass From 411c64b5eedea4f8030eb257fdd07a7268141ba1 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Wed, 17 Jun 2026 15:54:19 +0530 Subject: [PATCH 45/50] fix: relax sign-off validation to only block on explicit PENDING/FAIL Agents that participate but never mention SIGN-OFF: (e.g., Chief Architect doing analysis work) are not reviewers and should not block termination. Only agents that explicitly say SIGN-OFF: PENDING or SIGN-OFF: FAIL will block. This prevents the Coordinator from looping when worker agents complete without reviewer sign-off text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 18 +++++----- .../test_groupchat_orchestrator_internals.py | 13 +++++-- ...test_groupchat_orchestrator_termination.py | 35 +++++++++++++++++-- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 72ab72ec..44df2f2a 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -427,17 +427,19 @@ def _validate_sign_offs(self, conversation: list[Message]) -> tuple[bool, str]: and name != self.get_result_generator_name() ] - # Validate sign-offs + # Validate sign-offs — only for agents that explicitly attempted one. + # Agents like Chief Architect may participate without issuing a + # sign-off (they do analysis work, not reviews). We only block + # termination when an agent explicitly said SIGN-OFF: PENDING/FAIL. missing_or_invalid = [] for agent_name in reviewer_agents: status = sign_offs.get(agent_name) - if status != "PASS": - if status == "PENDING": - missing_or_invalid.append(f"{agent_name}: PENDING") - elif status == "FAIL": - missing_or_invalid.append(f"{agent_name}: FAIL") - else: - missing_or_invalid.append(f"{agent_name}: missing") + if status == "PENDING": + missing_or_invalid.append(f"{agent_name}: PENDING") + elif status == "FAIL": + missing_or_invalid.append(f"{agent_name}: FAIL") + # If status is None (agent never mentioned SIGN-OFF:), skip — + # not all agents are reviewers. if missing_or_invalid: reason = f"Cannot terminate: {', '.join(missing_or_invalid)}. All reviewers must have SIGN-OFF: PASS." diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index 6c9f8c5a..c4288269 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -239,12 +239,21 @@ def test_fail_blocks(self): assert ok is False assert "FAIL" in reason - def test_missing_blocks(self): + def test_missing_does_not_block(self): + """Agents that never mention SIGN-OFF: are not reviewers and should not block.""" orch = _make_orch() msgs = [_Msg(author_name="A", text="some text without signoff")] ok, reason = orch._validate_sign_offs(msgs) + assert ok is True + assert reason == "" + + def test_pending_blocks(self): + """Agents that explicitly say SIGN-OFF: PENDING should block.""" + orch = _make_orch() + msgs = [_Msg(author_name="A", text="SIGN-OFF: PENDING")] + ok, reason = orch._validate_sign_offs(msgs) assert ok is False - assert "missing" in reason + assert "PENDING" in reason def test_excludes_coordinator_and_resultgenerator(self): orch = _make_orch() diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py index 0117b791..f57e9fca 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py @@ -61,11 +61,12 @@ async def _run(): asyncio.run(_run()) -def test_coordinator_complete_rejected_when_signoffs_missing(): +def test_coordinator_complete_accepted_when_no_explicit_signoffs(): + """Agents that never mention SIGN-OFF: are treated as non-reviewers.""" async def _run(): orch = _make_orchestrator() - # Agent participated but never produced a SIGN-OFF. + # Agent participated but never produced a SIGN-OFF — should NOT block. orch._conversation = [ _Msg(author_name="AKS Expert", text="Reviewed; looks good."), ] @@ -84,6 +85,36 @@ async def _run(): await orch._complete_agent_response("Coordinator", callback=None) + # No explicit SIGN-OFF: in any message → termination allowed. + assert orch._termination_requested is True + + asyncio.run(_run()) + + +def test_coordinator_complete_rejected_when_signoffs_pending(): + """Agents with explicit SIGN-OFF: PENDING should block termination.""" + async def _run(): + orch = _make_orchestrator() + + # Agent participated and said SIGN-OFF: PENDING → should block. + orch._conversation = [ + _Msg(author_name="AKS Expert", text="SIGN-OFF: PENDING - need more info"), + ] + + orch._current_agent_start_time = datetime.now() + orch._current_agent_response = [ + json.dumps( + { + "selected_participant": None, + "instruction": "complete", + "finish": False, + "final_message": "done", + } + ) + ] + + await orch._complete_agent_response("Coordinator", callback=None) + assert orch._termination_requested is False asyncio.run(_run()) From 3e1685e55f09ed25641fca8387bdbf39ac416355 Mon Sep 17 00:00:00 2001 From: Prachig-Microsoft Date: Wed, 17 Jun 2026 16:14:34 +0530 Subject: [PATCH 46/50] fix: make sign-off validation log-only to prevent termination deadlock With selection_func routing, rejecting finish creates an unrecoverable loop. The Coordinator's prompt already handles sign-off logic. Keep validation for observability (log warnings) but always accept termination. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework/groupchat_orchestrator.py | 57 +++++-------------- .../test_groupchat_orchestrator_internals.py | 13 +---- ...test_groupchat_orchestrator_termination.py | 15 +++-- 3 files changed, 23 insertions(+), 62 deletions(-) diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 44df2f2a..5551430f 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -310,12 +310,6 @@ def __init__( # Snapshot of progress_counter at the time we last saw _last_coordinator_selection. self._last_coordinator_selection_progress: int = 0 - # Track consecutive finish rejections (sign-off validation failures). - # When the Coordinator keeps saying finish=true but sign-offs are - # incomplete, prevent infinite loops by force-terminating after a threshold. - self._finish_rejection_count: int = 0 - self._max_finish_rejections: int = 5 - def _request_forced_termination( self, *, reason: str, termination_type: str ) -> None: @@ -427,19 +421,17 @@ def _validate_sign_offs(self, conversation: list[Message]) -> tuple[bool, str]: and name != self.get_result_generator_name() ] - # Validate sign-offs — only for agents that explicitly attempted one. - # Agents like Chief Architect may participate without issuing a - # sign-off (they do analysis work, not reviews). We only block - # termination when an agent explicitly said SIGN-OFF: PENDING/FAIL. + # Validate sign-offs missing_or_invalid = [] for agent_name in reviewer_agents: status = sign_offs.get(agent_name) - if status == "PENDING": - missing_or_invalid.append(f"{agent_name}: PENDING") - elif status == "FAIL": - missing_or_invalid.append(f"{agent_name}: FAIL") - # If status is None (agent never mentioned SIGN-OFF:), skip — - # not all agents are reviewers. + if status != "PASS": + if status == "PENDING": + missing_or_invalid.append(f"{agent_name}: PENDING") + elif status == "FAIL": + missing_or_invalid.append(f"{agent_name}: FAIL") + else: + missing_or_invalid.append(f"{agent_name}: missing") if missing_or_invalid: reason = f"Cannot terminate: {', '.join(missing_or_invalid)}. All reviewers must have SIGN-OFF: PASS." @@ -1273,12 +1265,12 @@ async def _complete_agent_response( ) if coordinator_signaled_stop: - # Only enforce PASS sign-offs when Coordinator claims success completion. + # Log sign-off status for observability, but do NOT block + # termination. With selection_func routing, rejecting finish + # creates an unrecoverable loop (Coordinator keeps saying + # finish=true, selection_func routes back to Coordinator). + # The Coordinator's prompt already handles sign-off logic. if instruction == "complete": - # Use the reconstructed conversation from agent_responses - # because self._conversation (populated only by the final - # output event) is still empty at this point in the - # streaming loop. sign_off_conversation = ( self._conversation if self._conversation @@ -1286,28 +1278,10 @@ async def _complete_agent_response( ) is_valid, reason = self._validate_sign_offs(sign_off_conversation) if not is_valid: - self._finish_rejection_count += 1 logger.warning( - "Termination rejected for success completion (%d/%d): %s. Workflow continues.", - self._finish_rejection_count, - self._max_finish_rejections, + "Sign-off validation note: %s (proceeding with termination)", reason, ) - if self._finish_rejection_count >= self._max_finish_rejections: - logger.warning( - "Max finish rejections reached (%d). Force-terminating.", - self._finish_rejection_count, - ) - self._request_forced_termination( - reason=( - f"Coordinator attempted finish {self._finish_rejection_count} times " - f"but sign-off validation kept failing: {reason}" - ), - termination_type="hard_timeout", - ) - return - # Do NOT set _termination_requested. - return self._termination_requested = True self._termination_final_message = manager_response.final_message @@ -1321,9 +1295,6 @@ async def _complete_agent_response( and selected and selected.lower() != "none" ): - # Non-finish routing: reset finish rejection counter since - # Coordinator is doing productive work (selecting participants). - self._finish_rejection_count = 0 # Record invocation time for non-termination coordinator selections self._agent_invoked_at[selected] = completed_at except Exception as exc: diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py index c4288269..6c9f8c5a 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_internals.py @@ -239,21 +239,12 @@ def test_fail_blocks(self): assert ok is False assert "FAIL" in reason - def test_missing_does_not_block(self): - """Agents that never mention SIGN-OFF: are not reviewers and should not block.""" + def test_missing_blocks(self): orch = _make_orch() msgs = [_Msg(author_name="A", text="some text without signoff")] ok, reason = orch._validate_sign_offs(msgs) - assert ok is True - assert reason == "" - - def test_pending_blocks(self): - """Agents that explicitly say SIGN-OFF: PENDING should block.""" - orch = _make_orch() - msgs = [_Msg(author_name="A", text="SIGN-OFF: PENDING")] - ok, reason = orch._validate_sign_offs(msgs) assert ok is False - assert "PENDING" in reason + assert "missing" in reason def test_excludes_coordinator_and_resultgenerator(self): orch = _make_orch() diff --git a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py index f57e9fca..f560a044 100644 --- a/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py +++ b/src/processor/src/tests/unit/libs/agent_framework/test_groupchat_orchestrator_termination.py @@ -61,12 +61,13 @@ async def _run(): asyncio.run(_run()) -def test_coordinator_complete_accepted_when_no_explicit_signoffs(): - """Agents that never mention SIGN-OFF: are treated as non-reviewers.""" +def test_coordinator_complete_accepted_even_without_signoffs(): + """Sign-off validation is log-only; termination proceeds regardless.""" async def _run(): orch = _make_orchestrator() - # Agent participated but never produced a SIGN-OFF — should NOT block. + # Agent participated but never produced a SIGN-OFF. + # With log-only validation, termination should still proceed. orch._conversation = [ _Msg(author_name="AKS Expert", text="Reviewed; looks good."), ] @@ -85,18 +86,16 @@ async def _run(): await orch._complete_agent_response("Coordinator", callback=None) - # No explicit SIGN-OFF: in any message → termination allowed. assert orch._termination_requested is True asyncio.run(_run()) -def test_coordinator_complete_rejected_when_signoffs_pending(): - """Agents with explicit SIGN-OFF: PENDING should block termination.""" +def test_coordinator_complete_accepted_with_pending_signoffs(): + """Sign-off validation is log-only; PENDING sign-offs don't block.""" async def _run(): orch = _make_orchestrator() - # Agent participated and said SIGN-OFF: PENDING → should block. orch._conversation = [ _Msg(author_name="AKS Expert", text="SIGN-OFF: PENDING - need more info"), ] @@ -115,7 +114,7 @@ async def _run(): await orch._complete_agent_response("Coordinator", callback=None) - assert orch._termination_requested is False + assert orch._termination_requested is True asyncio.run(_run()) From 727bbb7515d9917addef342cdb3929820dd485c8 Mon Sep 17 00:00:00 2001 From: llPriyanka <40491221+llPriyanka@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:17:34 +0000 Subject: [PATCH 47/50] created new uv lock file --- src/processor/uv.lock | 1647 +++++++++++++++++++++-------------------- 1 file changed, 829 insertions(+), 818 deletions(-) diff --git a/src/processor/uv.lock b/src/processor/uv.lock index 190ec21c..d9e98a2f 100644 --- a/src/processor/uv.lock +++ b/src/processor/uv.lock @@ -18,7 +18,7 @@ overrides = [ [[package]] name = "a2a-sdk" -version = "1.0.2" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "culsans", marker = "python_full_version < '3.13'" }, @@ -31,21 +31,21 @@ dependencies = [ { name = "protobuf" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/f3/1c312eae0298542eef1a096be378a3ad2d20b171ea0ac6be26b81f542720/a2a_sdk-1.0.2.tar.gz", hash = "sha256:e4ee4dd509894c32c9a6df728319875fa4f049e70ae82476fa447353e3a4b648", size = 375193, upload-time = "2026-04-24T13:50:24.303Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/7e/8ac10bbf8b15b16574355f39b17dbdf617a282c27b41c7ff2116e30336df/a2a_sdk-1.1.0.tar.gz", hash = "sha256:e8102dad1b36709dbdc3d19319e38e6dfa3b3a79c30416030eb2d482576be204", size = 375726, upload-time = "2026-05-29T09:34:43.015Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/03/58c92a44e7b94a42614880df2365f074969e47067c4c736e31e855aca2fd/a2a_sdk-1.0.2-py3-none-any.whl", hash = "sha256:4dbc083b6808ee28207ac6daad263360f87612c37b2d06f5521efb530318141c", size = 234302, upload-time = "2026-04-24T13:50:22.412Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ea/3a5b160cfd51c67759b08748051094d9365ceff18127633d0021950c9860/a2a_sdk-1.1.0-py3-none-any.whl", hash = "sha256:d7f5846caf18033d8bf3108b11ec827dd8dd32f867c98848ede0e39474be93be", size = 241886, upload-time = "2026-05-29T09:34:41.484Z" }, ] [[package]] name = "ag-ui-protocol" -version = "0.1.18" +version = "0.1.19" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/d7/5711eada86da9bd7684e58645653a1693ef20b66cc3efbb1deeafef80f8d/ag_ui_protocol-0.1.18.tar.gz", hash = "sha256:b37c672c3fd6bac12b316c39f45ad9db9f137bbb885489c79f268507029a22ff", size = 9937, upload-time = "2026-04-21T20:44:59.151Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/10/4ad299267a7d04b89935aa99eef62979758fcf95aee9f8bb5d70c35b1be1/ag_ui_protocol-0.1.19.tar.gz", hash = "sha256:43c27f60d41712dcad0e9e0a203cbdf1c8e248b22417374c5c68321c448af4ea", size = 10720, upload-time = "2026-06-02T17:26:15.627Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/74/913c9b8fc566c6da650aecbddf25a5d8186b54138df265eb9eb546f56141/ag_ui_protocol-0.1.18-py3-none-any.whl", hash = "sha256:d151c0f0a34160647f1571163f7185746f4326b15a56d1560de5082a7a0e7a12", size = 12607, upload-time = "2026-04-21T20:45:00.097Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0a/bcad8116eb058e4b4a305e3fc37ebd7efc879deeb86b854f1c5b8b6e97dd/ag_ui_protocol-0.1.19-py3-none-any.whl", hash = "sha256:898843b1410d378824da0c6a776486288b9c5828689d0bf563118868e37f390f", size = 13490, upload-time = "2026-06-02T17:26:16.313Z" }, ] [[package]] @@ -136,8 +136,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "agent-framework-core" }, { name = "agent-framework-durabletask" }, - { name = "azure-functions", version = "1.26.0b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "azure-functions", version = "2.2.0b2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "azure-functions", version = "1.26.0b3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "azure-functions", version = "2.2.0b5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "azure-functions-durable" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/0e/59c4c45c380b4d0dcfb71be45ec60a8d52b271979b5cf9e5be1f9e974653/agent_framework_azurefunctions-1.0.0b260130.tar.gz", hash = "sha256:b6a971036c7088a61e5079549f11e0c7972b955452bdb6d576769ed8da27b920", size = 16340, upload-time = "2026-01-30T19:01:06.649Z" } @@ -324,14 +324,14 @@ wheels = [ [[package]] name = "aiofile" -version = "3.9.0" +version = "3.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "caio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/41/2fea7e193e061ce54eacc3b7bc0e6a99e4fcff43c78cf0a76dd781ed8334/aiofile-3.11.1.tar.gz", hash = "sha256:1f91912c6643d2a4e49ca4ae3514f0bf3867ce948a36d99a6411b8f4755f4cf9", size = 19342, upload-time = "2026-05-16T08:18:33.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, + { url = "https://files.pythonhosted.org/packages/67/cd/0d76dfc5de72bde52f55f53e925c7d152d9c7906634ec1e0cbc7e8d4ad93/aiofile-3.11.1-py3-none-any.whl", hash = "sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9", size = 20446, upload-time = "2026-05-16T08:18:32.051Z" }, ] [[package]] @@ -345,11 +345,11 @@ wheels = [ [[package]] name = "aiohappyeyeballs" -version = "2.6.1" +version = "2.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, ] [[package]] @@ -439,16 +439,16 @@ wheels = [ [[package]] name = "aiologic" -version = "0.16.0" +version = "0.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio", marker = "python_full_version < '3.13'" }, + { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, - { name = "wrapt", marker = "python_full_version < '3.13'" }, + { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/13/50b91a3ea6b030d280d2654be97c48b6ed81753a50286ee43c646ba36d3c/aiologic-0.16.0.tar.gz", hash = "sha256:c267ccbd3ff417ec93e78d28d4d577ccca115d5797cdbd16785a551d9658858f", size = 225952, upload-time = "2025-11-27T23:48:41.195Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a7/809482759f40079f4c4328c7318bf569ae25d457f5017aad30a1b9aafedc/aiologic-0.17.0.tar.gz", hash = "sha256:65aa058e858c94cd208badb188e7f00b54dcabb3ba85b34f794db98074d108b9", size = 251625, upload-time = "2026-06-14T12:24:35.367Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/27/206615942005471499f6fbc36621582e24d0686f33c74b2d018fcfd4fe67/aiologic-0.16.0-py3-none-any.whl", hash = "sha256:e00ce5f68c5607c864d26aec99c0a33a83bdf8237aa7312ffbb96805af67d8b6", size = 135193, upload-time = "2025-11-27T23:48:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6b/5f75d6194b597ac32bbdbb7b524a28fb1fa98bd0ddcefce94b313a818cc0/aiologic-0.17.0-py3-none-any.whl", hash = "sha256:1bf4d3e4314df2bcb06a9e696417204e206ab50e10ec98d28d157e2e57634f74", size = 161084, upload-time = "2026-06-14T12:24:34.146Z" }, ] [[package]] @@ -484,7 +484,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.98.1" +version = "0.109.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -496,22 +496,22 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/60/a9e4426dfe594e5eec8a9757d48e3d8dcf529a0a35a4fc8aefa352bd95fe/anthropic-0.98.1.tar.gz", hash = "sha256:62205edec42f5877df63d58be8e9443843d3e032215836e228fba1f59514a433", size = 725085, upload-time = "2026-05-04T21:40:39.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/b7/9a8e2f79011e89dd6eeb599c27332aed765dac9d6fbee3a55e68e4e3ec25/anthropic-0.109.2.tar.gz", hash = "sha256:d37db299597c7bc124b49b767ff135f1e6456b64af2b2fad4b63b2a1df333cf0", size = 927559, upload-time = "2026-06-15T17:30:25.024Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/6f/7f7f80f714e6de0784518f1999f71fd632076aefd3e22fe0ccd27ca9571f/anthropic-0.98.1-py3-none-any.whl", hash = "sha256:107ebf954415382fdcea6a94f9cf334a53199ad64794403590dc55366cefcc28", size = 699604, upload-time = "2026-05-04T21:40:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f2/bee5de8a2699fc8a3cce34d61c7a2626a2c310ddde7ea5611327eb0ddbe9/anthropic-0.109.2-py3-none-any.whl", hash = "sha256:e0fb4ca5df0ed983248c9c6c3242adc81d9cfddb8725902da53698554117abac", size = 923800, upload-time = "2026-06-15T17:30:23.124Z" }, ] [[package]] name = "anyio" -version = "4.13.0" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, ] [[package]] @@ -683,7 +683,7 @@ wheels = [ [[package]] name = "azure-functions" -version = "1.26.0b1" +version = "1.26.0b3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.13'", @@ -691,14 +691,14 @@ resolution-markers = [ dependencies = [ { name = "werkzeug", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/a1/dcc891ab6595a9933b92559eeaac7a322b02efc3bc2b8c46bdefb96cba2b/azure_functions-1.26.0b1.tar.gz", hash = "sha256:f12d33858e91f84a03369fd3c35a9edf6d7ff2c33314e7de5f78e81cb3473e1a", size = 152503, upload-time = "2026-04-14T15:28:20.678Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/8d/fb56179e6e550e222446444c7d519c78d3c94760cc877d1062bd264ac744/azure_functions-1.26.0b3.tar.gz", hash = "sha256:e40684bed006f03473df8d87407bee4666e89f4805242481bf7ecca304909b67", size = 168636, upload-time = "2026-06-02T16:18:39.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/3d/702c510f5aec4331de1df3acbbc0bf4574a04209422e142d1430f8671478/azure_functions-1.26.0b1-py3-none-any.whl", hash = "sha256:4101cec4e129ac492b3d4658c9d4f22b04154c854fd1ef75c0faafdbb60c2796", size = 117544, upload-time = "2026-04-14T15:28:19.535Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/82c1df422d82e279d17db33a4989f117f87d5b8076203918f3108dbad1b9/azure_functions-1.26.0b3-py3-none-any.whl", hash = "sha256:75454ff884fb422831c42fba0bf464c3535dc73433e13edadf7a7fa96853aeea", size = 124908, upload-time = "2026-06-02T16:18:37.658Z" }, ] [[package]] name = "azure-functions" -version = "2.2.0b2" +version = "2.2.0b5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", @@ -707,9 +707,9 @@ resolution-markers = [ dependencies = [ { name = "werkzeug", marker = "python_full_version >= '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/b9/71c0ba96e1951ad41c677d93c22041b8f56ed121734c6210c57b235e6073/azure_functions-2.2.0b2.tar.gz", hash = "sha256:51097332654ede6be3ed53a7fb9512f5323f222940dcc966827962f70e7ea6af", size = 155316, upload-time = "2026-04-22T17:13:38.71Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/17/8385e0cf4dd0f919cd407093519ae8a2a903569707c426bdddbe338380de/azure_functions-2.2.0b5.tar.gz", hash = "sha256:693767f6ee067d9afdbd20a47b24a9cfd0ab74cacfd04f8b3764fd2c5a845e95", size = 168505, upload-time = "2026-06-01T19:11:48.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/3e/12391f538cf695dc3b824cef35e50794895bafabe886b167c84dd0b53354/azure_functions-2.2.0b2-py3-none-any.whl", hash = "sha256:22705ceddc50af72d551fba90cefe59453f2cc5f354340c1f61058be7784a8cd", size = 119590, upload-time = "2026-04-22T17:13:36.742Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6a/565464f20e9ae6b497a09e386c6988b90c8cb8da8364a680993a68c49f4a/azure_functions-2.2.0b5-py3-none-any.whl", hash = "sha256:a1035ce6bb04ccde7016225a3411f2d5eed4ad5f5c2aa855c1189676866b4aa8", size = 124883, upload-time = "2026-06-01T19:11:47.339Z" }, ] [[package]] @@ -718,8 +718,8 @@ version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, - { name = "azure-functions", version = "1.26.0b1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "azure-functions", version = "2.2.0b2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "azure-functions", version = "1.26.0b3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "azure-functions", version = "2.2.0b5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "furl" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, @@ -786,7 +786,7 @@ wheels = [ [[package]] name = "azure-monitor-opentelemetry-exporter" -version = "1.0.0b52" +version = "1.0.0b53" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -796,9 +796,9 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "psutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/7e/bfc03436b88c48f5adc21a3ebbf4392b6b7fbbfe33ef3b1e88d07ba9f380/azure_monitor_opentelemetry_exporter-1.0.0b52.tar.gz", hash = "sha256:7eac679fca32dee9e426df65f2a538161db4514fc322fc66107f7826567d86e1", size = 326179, upload-time = "2026-05-11T22:47:02.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/64/875f13849fe2e3832ceda6a218fa5422a25e72c1b86623a8514f541a8c60/azure_monitor_opentelemetry_exporter-1.0.0b53.tar.gz", hash = "sha256:1274e9008909414a25c6287185a6c5a884209705b6e651a1ffddfbdab3b76e52", size = 335614, upload-time = "2026-06-08T15:54:22.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/e8/d13e6a74c98ecc3011bce9ab09fc2e75aec48ab46288f72be57c2fa21460/azure_monitor_opentelemetry_exporter-1.0.0b52-py2.py3-none-any.whl", hash = "sha256:a38c503e5e2cc0ec8a4bf336b23cce23488719f5361a45cdd01a514080f0e7fc", size = 244751, upload-time = "2026-05-11T22:47:04.304Z" }, + { url = "https://files.pythonhosted.org/packages/5f/dc/6dbff881ac739999bc31f166504ed31a2e0fef45cc59a3c1b2386a4b4abe/azure_monitor_opentelemetry_exporter-1.0.0b53-py2.py3-none-any.whl", hash = "sha256:e2faadaf203369f25cb96df2a75017bd7f11655b033200c5867151f8fb2a6ba7", size = 249226, upload-time = "2026-06-08T15:54:24.413Z" }, ] [[package]] @@ -881,11 +881,11 @@ wheels = [ [[package]] name = "cachetools" -version = "7.1.1" +version = "7.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/e2/85f227594656000ff4d8adadae91a21f536d4a84c6c716a86bd6685874be/cachetools-7.1.1.tar.gz", hash = "sha256:27bdf856d68fd3c71c26c01b5edc312124ed427524d1ddb31aa2b7746fe20d4b", size = 40202, upload-time = "2026-05-03T20:00:29.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/0f/f897abe4ea0a8c408ae65c8c83bffab4936ad65d6032d4fb4cd35bbdc3ee/cachetools-7.1.1-py3-none-any.whl", hash = "sha256:0335cd7a0952d2b22327441fb0628139e234c565559eeb91a8a4ac7551c5353d", size = 16775, upload-time = "2026-05-03T20:00:27.857Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, ] [[package]] @@ -911,11 +911,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] @@ -1059,14 +1059,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.3" +version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, ] [[package]] @@ -1074,7 +1074,7 @@ name = "clr-loader" version = "0.2.10" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "python_full_version < '3.14'" }, + { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" } wheels = [ @@ -1092,55 +1092,52 @@ wheels = [ [[package]] name = "cryptography" -version = "48.0.0" +version = "49.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, - { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, - { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, ] [[package]] @@ -1148,7 +1145,7 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic", marker = "python_full_version < '3.13'" }, + { name = "aiologic" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } @@ -1172,11 +1169,11 @@ wheels = [ [[package]] name = "distlib" -version = "0.4.0" +version = "0.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, ] [[package]] @@ -1208,7 +1205,7 @@ wheels = [ [[package]] name = "durabletask" -version = "1.4.0" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asyncio" }, @@ -1216,22 +1213,22 @@ dependencies = [ { name = "packaging" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/25/11d70b07723587a0b95fb57b5817627c9e605554b874697e5aeee3e5466d/durabletask-1.4.0.tar.gz", hash = "sha256:639138c10e2687a485ee94d218c27f8dc193376367dce9617f1ca2ec1cc8f021", size = 97252, upload-time = "2026-04-08T18:49:26.348Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/78/099bc761bec57fd8fdd906058bc7a9c1b8caf8a0bfff774c25162ef1a20c/durabletask-1.5.0.tar.gz", hash = "sha256:93a80f6fb04d8fdad90b0d69518d27a70947cceb5ae1c6fb6a286ff481d19c16", size = 112887, upload-time = "2026-06-05T22:36:51.031Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/3f/7250be7683aa6e9e89324db549e2b44cb6db7904cd315024933a23405e07/durabletask-1.4.0-py3-none-any.whl", hash = "sha256:75e11407bf24f045e32ef26b5e753f49f64fee822c8c9bfc5184a0911cb0969c", size = 107934, upload-time = "2026-04-08T18:49:24.856Z" }, + { url = "https://files.pythonhosted.org/packages/96/67/c0a33b3208ad862eef23e1ddcc452b3759af0d24e73b91f3c517c43d863d/durabletask-1.5.0-py3-none-any.whl", hash = "sha256:58371340fe37714bf662d9c9d05791e038203e6b2e7888e7a4b9927af0999711", size = 125164, upload-time = "2026-06-05T22:36:52.259Z" }, ] [[package]] name = "durabletask-azuremanaged" -version = "1.4.0" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-identity" }, { name = "durabletask" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/a9/18501dc091867a9bb5a7d184c69f3fac14294f34dea2363aa9379eeeedc3/durabletask_azuremanaged-1.4.0.tar.gz", hash = "sha256:739cde74ecdacf732fa4a9a40c0afba5d3185c5e575a6883d303c5a112f2c34a", size = 5657, upload-time = "2026-04-08T19:22:27.732Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/53/653c4a4a120906cfff9c824583bd9398528657ba683bfe9d12daac064c94/durabletask_azuremanaged-1.5.0.tar.gz", hash = "sha256:5af9fabb67a555039087b5242d628587b5179a2cfa6609cfbc5b227a0e8669e6", size = 5944, upload-time = "2026-06-05T22:37:10.299Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/95/00ef2b2e0dd62fee6dc411aa1c4071ac55e54bcbd47a1384722e0ba54f42/durabletask_azuremanaged-1.4.0-py3-none-any.whl", hash = "sha256:80a0255afa7b61c01886d82dc22b75188b786f2454ea9f1a09dac10888a3c131", size = 7852, upload-time = "2026-04-08T19:22:26.624Z" }, + { url = "https://files.pythonhosted.org/packages/e1/22/b6b7173209f03b458fae9ae98d20b074bf0a1788286227489d9bd23df302/durabletask_azuremanaged-1.5.0-py3-none-any.whl", hash = "sha256:260249dc537010f01b12215f57072a2dbdf00892132aa37a543dcc66712502af", size = 8314, upload-time = "2026-06-05T22:37:11.247Z" }, ] [[package]] @@ -1261,7 +1258,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.136.1" +version = "0.137.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -1270,9 +1267,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/29/cc5819dc24d3daa80cdaa1aec023bf8652a70dd7fd1c96b0b225c99a7690/fastapi-0.137.2.tar.gz", hash = "sha256:b9d893bebc97dcfbdcb1917e88a292d062844ea19445a5fa4f7eb28c4baea9e3", size = 410332, upload-time = "2026-06-18T06:58:24.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ed/0c6b644e99fb5697d8bdcd36cdb47c52e77a63fc7a1514b1f03a6ecab955/fastapi-0.137.2-py3-none-any.whl", hash = "sha256:791d36261e916a98b25ac85ee591bc3db159394070f6d3d096d94fb378f60ce2", size = 122252, upload-time = "2026-06-18T06:58:26.074Z" }, ] [[package]] @@ -1310,11 +1307,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.29.0" +version = "3.29.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, + { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, ] [[package]] @@ -1421,7 +1418,7 @@ wheels = [ [[package]] name = "google-api-core" -version = "2.30.3" +version = "2.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, @@ -1430,34 +1427,34 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/22/155cadf1d49272a9cf48f3168c0f3874fa13397297e611a5ea00cd093880/google_api_core-2.31.0.tar.gz", hash = "sha256:2be84ee0f584c48e6bde1b36766e23348b361fb7e55e56135fc76ce1c397f9c2", size = 176492, upload-time = "2026-06-03T14:52:17.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" }, + { url = "https://files.pythonhosted.org/packages/86/40/9bdbb60b03a332bd45acb8703da08bbc27d991d35286b62e42acc86d243a/google_api_core-2.31.0-py3-none-any.whl", hash = "sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab", size = 173102, upload-time = "2026-06-03T14:51:26.729Z" }, ] [[package]] name = "google-auth" -version = "2.50.0" +version = "2.55.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/18/238d7021d151bdab868f23433817b027dd759135202f4dfce0670d1230ca/google_auth-2.50.0.tar.gz", hash = "sha256:f35eafb191195328e8ce10a7883970877e7aeb49c2bfaa54aa0e394316d353d0", size = 336523, upload-time = "2026-04-30T21:19:29.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/1c/70b23fc52b2bb3c70b379f3bd05c4a60ab3a873e30c6bd21c57e0154848a/google_auth-2.55.0.tar.gz", hash = "sha256:fcd3a130f575fa36403d38774af1c64a4fbfbca09215f0589d2372b5119697cb", size = 349379, upload-time = "2026-06-15T22:33:16.466Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/cf/4880c2137c14280b2f59975cdf12cc442bc0ae1f9ea473a26eaa0c146786/google_auth-2.50.0-py3-none-any.whl", hash = "sha256:04382175e28b94f49694977f0a792688b59a668def1499e9d8de996dc9ce5b15", size = 246495, upload-time = "2026-04-30T21:19:27.664Z" }, + { url = "https://files.pythonhosted.org/packages/44/71/c0321dc6d63d99946da45f7c06299b934e4f7f7da5c4f14d101bcb39adf1/google_auth-2.55.0-py3-none-any.whl", hash = "sha256:a17cef9dedf98c4ebae2fb0c48c8f75952c877cbc2efe09f329ef16c2783d88a", size = 252400, upload-time = "2026-06-15T22:33:14.992Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.74.0" +version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [[package]] @@ -1471,43 +1468,43 @@ wheels = [ [[package]] name = "grpcio" -version = "1.80.0" +version = "1.82.0rc1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, - { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, - { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, - { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, - { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, - { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, - { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, - { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, - { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, - { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, - { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, - { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, - { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, - { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, - { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, - { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, - { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, - { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, - { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, - { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/4320167963523abb65d7731ec1f5988ca4369bef11f1a3ff261fa0ae2124/grpcio-1.82.0rc1.tar.gz", hash = "sha256:bb8ff2c5b564823d0d1f1cfad8926a5c5226344f611ddca915452a9839d99277", size = 13194883, upload-time = "2026-06-17T04:01:25.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/fb/9a898553730680cd039d38df4c29340dc965f986141ea1350580c71c9cf0/grpcio-1.82.0rc1-cp312-cp312-linux_armv7l.whl", hash = "sha256:ab5eddfa69b9c00c9b9d4b920beefed108f1d1ecc7d4c432034c19af333eb6cf", size = 6144666, upload-time = "2026-06-17T03:59:59.164Z" }, + { url = "https://files.pythonhosted.org/packages/77/68/70614957671cfc56873212e53453234acd7d868a0ee9a9375cd666d04b08/grpcio-1.82.0rc1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:66ab43e0f2d24af3710bc20b8c6fffb0a75603549acbc52951114ee04b9de518", size = 11952602, upload-time = "2026-06-17T04:00:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/a557baa37f6b399820e72b55b3b027b75b2cce8a3525107da8c725981614/grpcio-1.82.0rc1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6f07018eeb61f2c4350bb46a67a32d50991c063373e7c249d80bc29fcf899e81", size = 6710554, upload-time = "2026-06-17T04:00:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/56/8e/22a4bae98c5bbe3332a52ecb5186b219a978b0a2e18f34d36d3af6dad9c7/grpcio-1.82.0rc1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0f85e745cd372f248f9cd7b62fe7b25b0b39dc8c990eec43d47817cf3045eb7f", size = 7450507, upload-time = "2026-06-17T04:00:07.191Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/d4702335b0c6d8dfd97f5f5388356356cace9bddd95e5ed8daf5457ba531/grpcio-1.82.0rc1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25d60cc203c492097e73844a7a7b49f8f37ffba8796ff0fdb561cc72df9cb80d", size = 6886906, upload-time = "2026-06-17T04:00:09.738Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/2b77cbb2ac5af963278878a6b61f68e18bcbc1a216ec282da7deeabd26e2/grpcio-1.82.0rc1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:acc4586e269a1ee9d1858ff46d8350be6d98af480d7b4ca0586031f35cb2fc72", size = 7501500, upload-time = "2026-06-17T04:00:12.07Z" }, + { url = "https://files.pythonhosted.org/packages/c8/16/0ee65d3c05121cc9587672d32250df458305a988a2a940ed28ba5a3bc487/grpcio-1.82.0rc1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9bf115ad35c21a833c010be15be66e0730db69d7cdaa23f0c31570df58939fa2", size = 8537067, upload-time = "2026-06-17T04:00:14.855Z" }, + { url = "https://files.pythonhosted.org/packages/94/99/c0ab7d8f17650aa957ad5a2dc43889018cf8fda32e53d5976415b0a019f0/grpcio-1.82.0rc1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60e3b011aa1a7ffd414c2a57f2cd6e86fc73f3d2e54e14cc2e2ab224284c95b5", size = 7914009, upload-time = "2026-06-17T04:00:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/22/2c/72c887f9cbb625d2adca69848f54ef1b41458eb97afdcd51000c8d9ef67e/grpcio-1.82.0rc1-cp312-cp312-win32.whl", hash = "sha256:c6ec64409c8d010b6d982d14f9458aacf00f29cfb1b74164ba35a09d08d9985d", size = 4240963, upload-time = "2026-06-17T04:00:20.621Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c0/31dd136de4951557715b9b18e08200baea3ec2db4edce1209c69467949d6/grpcio-1.82.0rc1-cp312-cp312-win_amd64.whl", hash = "sha256:867c057a00695d7dce0408dfe8af848995d7f604b68599aaf8b3aa28ae061317", size = 5001620, upload-time = "2026-06-17T04:00:23.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d4/e749e36c0d957fcd717b45dafb13baaac13eecfcc5860684020a739632cf/grpcio-1.82.0rc1-cp313-cp313-linux_armv7l.whl", hash = "sha256:4d01821d674f000419c4f024d9ee4b078a69f3800ad921824341617e652289ed", size = 6146209, upload-time = "2026-06-17T04:00:26.403Z" }, + { url = "https://files.pythonhosted.org/packages/ad/57/02b55bf3025ed4382d44697be0ae85326963615a0d2c1afeb17dd94f0414/grpcio-1.82.0rc1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0f568b4cc39b2f62c81d19522f7eb9f445854864ed92569753c829d6f07298bb", size = 11949296, upload-time = "2026-06-17T04:00:29.258Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/aa919596defb1476f86be967682c2d35f7ec28243d5df90274747b251cda/grpcio-1.82.0rc1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:503461041476e8ad20b0ec133d6c2d6d7a3a572d8247e4d55d0d82db1a83adb0", size = 6714824, upload-time = "2026-06-17T04:00:32.391Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e8/186174f75c98f421f7228e1d5d484a1245e5eb14d914805ebc29d7a84729/grpcio-1.82.0rc1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:22e92d5c014274fdd40e851c9d0c0ff560c2c8993f69150aa8f7badcece1c7d8", size = 7455067, upload-time = "2026-06-17T04:00:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b5/a78296611b9f32866a1c4734c7a23f52c690796a90a3b0650781e5dc65d0/grpcio-1.82.0rc1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:07de1d51c1d21012fdbf55b4881b39088fefccf073eb0c9e196e68d15ef99ceb", size = 6888683, upload-time = "2026-06-17T04:00:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/20/7e/ec0b970e199ea85be088316db36baded6388fc7fdfb1670548caa29888a7/grpcio-1.82.0rc1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc946230ddd72e69545b12a10b6700159c00ca10cd1292247796244a77aa21db", size = 7505400, upload-time = "2026-06-17T04:00:40.417Z" }, + { url = "https://files.pythonhosted.org/packages/45/0f/cf0c8425292e5ca7ca25d6b6380a723699b7c17bed166dde451cbd6ad9d7/grpcio-1.82.0rc1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:dba6765b4d71b821e2b2b1f7bc8bcfcc7d9ea46036482c17b370c207a96727c7", size = 8535513, upload-time = "2026-06-17T04:00:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6e/8cdf689eac595b3a637121c04d117d05cf678c81438e9ec417b768d7985d/grpcio-1.82.0rc1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f85aa36c71b95d80a6d8301499f9e32fcad961bc8e9f9410c30ab140fccf368f", size = 7910806, upload-time = "2026-06-17T04:00:46.898Z" }, + { url = "https://files.pythonhosted.org/packages/11/b3/a182581364e7ebc7e5bbcb16ad69faf6058d3eca4b8264584a5c5fa4469b/grpcio-1.82.0rc1-cp313-cp313-win32.whl", hash = "sha256:df1cabd2e881c8d972774ea2cc8009ffde482231fe915a086e9847980e1dd429", size = 4240319, upload-time = "2026-06-17T04:00:49.856Z" }, + { url = "https://files.pythonhosted.org/packages/c2/45/d2da1afc20929559eb9071d40d41877b4001ad867437bd2b71c32c37b96f/grpcio-1.82.0rc1-cp313-cp313-win_amd64.whl", hash = "sha256:84897a4d0349bc738a6414080ba7dd4588d2df84c19d95d75009bb2bb67f847a", size = 5001025, upload-time = "2026-06-17T04:00:52.433Z" }, + { url = "https://files.pythonhosted.org/packages/f0/44/03acb103cfba93c6dd07d99fcfe546a88def23ad063104e36292d7ef8e77/grpcio-1.82.0rc1-cp314-cp314-linux_armv7l.whl", hash = "sha256:3fd91e483e65a277a3db7800b980691efebbe0582e6cd43ef14c502e804de33c", size = 6146965, upload-time = "2026-06-17T04:00:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/8492645d7915024dcf542d03ded03302110e747b798abf7e68bcdde9a39a/grpcio-1.82.0rc1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:dbe71d2ff02fa096e6069a34e561561eec54f3bf9f193591264676371a48919b", size = 11955303, upload-time = "2026-06-17T04:00:58.081Z" }, + { url = "https://files.pythonhosted.org/packages/d6/99/39f0c51fc2625f8f91f1a8a2da1fcbe052c3cb692a0a7a0f35a798b2449e/grpcio-1.82.0rc1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da5193e684228e6fc886f59afd723f20b8ddae4fd99bfca48a912a1626ce5349", size = 6723217, upload-time = "2026-06-17T04:01:01.334Z" }, + { url = "https://files.pythonhosted.org/packages/74/86/a5463de87685a24369205a9005d08b8203217231fd0e8d7b2a3030b1e702/grpcio-1.82.0rc1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:030026c57ea908e1a7b012ecab6dedbc856dac632d12a3977ddbc8dde6610efa", size = 7454594, upload-time = "2026-06-17T04:01:05.178Z" }, + { url = "https://files.pythonhosted.org/packages/0c/38/7d420a87148e60af4335403812dccd663fc37825d64c419b30fef9359a88/grpcio-1.82.0rc1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f82fc5c61a1eaa07f0d19a1632530cf9b45aa92d00521a8e397016a08a95a32", size = 6889641, upload-time = "2026-06-17T04:01:07.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/66/60c5a5f379a5fe2a9546719ca376ffdd3b0e0531ca2fb0003272fcd45bd2/grpcio-1.82.0rc1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc5aaff87fd5a077fcb007d3e869d2fa17585ccf0db11047829818347cb41876", size = 7514334, upload-time = "2026-06-17T04:01:10.472Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fb/a797d851315b776c76c7d7044a1d78e3b9fdfb3ce4934703339cbef21440/grpcio-1.82.0rc1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4b4609c64d173e5f63c177bba0a1c4ca421b72e5d14dc287f3ea79eb3f0fc47e", size = 8536358, upload-time = "2026-06-17T04:01:13.335Z" }, + { url = "https://files.pythonhosted.org/packages/74/4b/6435cc50532675183e6a2be9b96d2b0168a73ba455e3fda1ff5d14fcaa85/grpcio-1.82.0rc1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:eb20fc64e5efd19e0e9b2f03e77e348229ade5e1beaced35929c9c02f58ac135", size = 7912667, upload-time = "2026-06-17T04:01:16.773Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/153558ddba64dc3d9cda2866b0df21f395f9bd16eb6df05722b36beeb208/grpcio-1.82.0rc1-cp314-cp314-win32.whl", hash = "sha256:8d895ca6cf862f1b830658d607d01d6fe15d2d4c426e0a1e4195ce0cd4601405", size = 4321944, upload-time = "2026-06-17T04:01:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c0/18250da493ddf95d84da6945e60b6702c1b2d0d37251ffb3456cb538f30a/grpcio-1.82.0rc1-cp314-cp314-win_amd64.whl", hash = "sha256:892c08a292241e38a740de509f2b0189f2940ffaa599d55ae4d4fc5ba4f1386a", size = 5141156, upload-time = "2026-06-17T04:01:22.201Z" }, ] [[package]] @@ -1556,31 +1553,38 @@ wheels = [ [[package]] name = "httptools" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, - { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, - { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, - { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, - { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, - { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, - { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, - { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, ] [[package]] @@ -1632,11 +1636,11 @@ wheels = [ [[package]] name = "idna" -version = "3.13" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -1692,14 +1696,14 @@ wheels = [ [[package]] name = "jaraco-functools" -version = "4.4.0" +version = "4.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/cf/ea4ef2920830dea3f5ab2ea4da6fb67724e6dca80ee2553788c3607243d0/jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03", size = 20272, upload-time = "2026-05-15T21:34:10.025Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, + { url = "https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4", size = 10594, upload-time = "2026-05-15T21:34:08.595Z" }, ] [[package]] @@ -1725,86 +1729,86 @@ wheels = [ [[package]] name = "jiter" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, - { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, - { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, - { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, - { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, - { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, - { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, - { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, - { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, - { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, - { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, - { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, - { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, - { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, - { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, - { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, - { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, - { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, - { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, - { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, - { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, - { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, - { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, - { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, - { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, - { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, - { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, - { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, - { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, - { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, - { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, - { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, - { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, - { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, - { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, - { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, - { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, - { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, - { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, + { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, + { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, + { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, + { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, + { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, + { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, ] [[package]] name = "joserfc" -version = "1.6.4" +version = "1.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/c6/de8fdbdfa75c8ca04fead38a82d573df8a82906e984c349d58665f459558/joserfc-1.6.4.tar.gz", hash = "sha256:34ce5f499bfcc5e9ad4cc75077f9278ab3227b71da9aaf28f9ab705f8a560d3c", size = 231866, upload-time = "2026-04-13T13:15:40.632Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/90/25cb27518750218e4f850be63d8bbb2343efaad1c01c3571aaa4b3c33bd7/joserfc-1.7.1.tar.gz", hash = "sha256:77d0b76514879c68c6f433bc5b7357a4ab72008ff1e33d8379fd11d72bd8ca81", size = 233181, upload-time = "2026-06-08T07:21:33.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/f7/210b27752e972edb36d239315b08d3eb6b14824cc4a590da2337d195260b/joserfc-1.6.4-py3-none-any.whl", hash = "sha256:3e4a22b509b41908989237a045e25c8308d5fd47ab96bdae2dd8057c6451003a", size = 70464, upload-time = "2026-04-13T13:15:39.259Z" }, + { url = "https://files.pythonhosted.org/packages/b3/00/fa62404c3e347f946faa13aa21085205f9cc06ad17671e37f81a51662ae8/joserfc-1.7.1-py3-none-any.whl", hash = "sha256:b3e3d655612e2e1ef67b2600f2f420e12e537b020208fab1761fad647319c164", size = 70423, upload-time = "2026-06-08T07:21:32.001Z" }, ] [[package]] @@ -1851,16 +1855,17 @@ wheels = [ [[package]] name = "jsonschema-path" -version = "0.4.6" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "attrs" }, { name = "pathable" }, { name = "pyyaml" }, { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/86/cfee6dd25843bec0760f456599a4f7e7e40221a934b9229fda0662c859bc/jsonschema_path-0.4.6.tar.gz", hash = "sha256:c89eb635f4d497c9ac328eeff359c489755838806a7d033510a692e9576f5c4b", size = 15302, upload-time = "2026-04-27T18:57:08.412Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/79/cd02a4df6d9270efdc7d3feefe6edd730b0820c39eeaa107a2faee8322d5/jsonschema_path-0.5.0.tar.gz", hash = "sha256:493b156ba895c97602655b620a8456caa2ce08c1aa389f5a7addec065e6e855c", size = 19597, upload-time = "2026-05-19T20:45:00.971Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/43/3d3065c05a04bb550c143bfbb8e4fd7022cd327e1082bf257bac74923783/jsonschema_path-0.4.6-py3-none-any.whl", hash = "sha256:451354b5311fa955c3144e6e4e255388c751c0121c5570ec5bb9291dd42d08c9", size = 19565, upload-time = "2026-04-27T18:57:06.792Z" }, + { url = "https://files.pythonhosted.org/packages/04/2c/9e69d73c4297508be9e3b64a970ea3971b3eb8db64ffc5802d40bd25981f/jsonschema_path-0.5.0-py3-none-any.whl", hash = "sha256:2790a070bc7abb08ea3dbe4d340ece4efadf639223001f020c7503229ba068e2", size = 24077, upload-time = "2026-05-19T20:44:59.225Z" }, ] [[package]] @@ -1903,14 +1908,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -2017,7 +2022,7 @@ wheels = [ [[package]] name = "mem0ai" -version = "2.0.1" +version = "2.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openai" }, @@ -2028,38 +2033,38 @@ dependencies = [ { name = "qdrant-client" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/03/3dc535b98310912e4f10083acdbbca2c5e2dfccb3921230a460464f9f4d0/mem0ai-2.0.1.tar.gz", hash = "sha256:070dbc3f1f332c8908379b42a81ab3a96ab169f2f9fa537e6ac719df02478f9c", size = 211820, upload-time = "2026-04-25T17:39:06.744Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/e8/813101298d681510a000ae5ddb4a994d78a2e20165fc4acf5fa482630502/mem0ai-2.0.7.tar.gz", hash = "sha256:c254c5b851f9bd95dc1f136a2f14f10a980afa607bff381d775b9846442c44ca", size = 229204, upload-time = "2026-06-17T16:26:19.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/96/e6153262f1464f4d412208732fea31496d9983ade155dd2c5c5492f8f8a4/mem0ai-2.0.1-py3-none-any.whl", hash = "sha256:63da5f50ad0c2514e27c2f380ef03f2ceea47c97873096ddfd997785b58043ec", size = 299461, upload-time = "2026-04-25T17:39:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/a4/db/b4492c154e7b7bbde5d763ad53675a1735095681a4c1890f0017880d8819/mem0ai-2.0.7-py3-none-any.whl", hash = "sha256:02bfef7932297d77a8a13ddb4f2232a781a8f827004026586c947b7e3ae0ff24", size = 319881, upload-time = "2026-06-17T16:26:17.709Z" }, ] [[package]] name = "microsoft-agents-activity" -version = "0.10.0.dev4" +version = "1.1.0.dev7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/9c/d43e35476c65aed92b8edfa6a1b4b4fb199cc0b4b1aa2baf5e64599dc888/microsoft_agents_activity-0.10.0.dev4.tar.gz", hash = "sha256:5172ef444e7143592488667145697f7c852179330aeeb91caf1b5ac720a8ab23", size = 62600, upload-time = "2026-05-02T09:10:03.04Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/19/776fa3a7f63470fe2ee3641ffa810ecf2fe15580f155114d59446cba0b76/microsoft_agents_activity-1.1.0.dev7.tar.gz", hash = "sha256:55ced3bc4f3df634de125723323f29e99cbf44481cac2d1e103d516cbc6c2e3a", size = 62799, upload-time = "2026-06-16T09:13:54.072Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/07/9e0e6a71508b42c5c60c46c68136ce5d7c4f48eb1b06b788693450651675/microsoft_agents_activity-0.10.0.dev4-py3-none-any.whl", hash = "sha256:133301f41e4e186d59b4c9de21bcd87731ad2dbdd45adcde5820bb6939fe748b", size = 134838, upload-time = "2026-05-02T09:10:14.273Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ac/dcd98767a80e7040b087eefcef6724435d0e5bada7bc36e57056cfb6713e/microsoft_agents_activity-1.1.0.dev7-py3-none-any.whl", hash = "sha256:de4c76ea207113c85cafcdf6e0d4429342cdc17176bac3285280eb5b4164d031", size = 135004, upload-time = "2026-06-16T09:14:05.141Z" }, ] [[package]] name = "microsoft-agents-copilotstudio-client" -version = "0.10.0.dev4" +version = "1.1.0.dev7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microsoft-agents-hosting-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/1b/ac3291d1e0a2a36b6b155e583a816f26623c8201c01b6c208b911badd5ab/microsoft_agents_copilotstudio_client-0.10.0.dev4.tar.gz", hash = "sha256:141300abde93463d071b3544c946766e9a97159b36cca826ed484bff408da984", size = 27276, upload-time = "2026-05-02T09:10:05.527Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/0b/4ca87e9a390f0016981898964d289ff788dfcdd819a7d7f3e0da71284aab/microsoft_agents_copilotstudio_client-1.1.0.dev7.tar.gz", hash = "sha256:acfa7d0aa0034ead271306d6ee93619927f081b6e82ca0c5de54ec7c39895957", size = 27470, upload-time = "2026-06-16T09:13:56.262Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/48/6fdb7cbf84fb084adcb1749996be3b83bc6fc50b1c1e53feb895c293d22c/microsoft_agents_copilotstudio_client-0.10.0.dev4-py3-none-any.whl", hash = "sha256:f275aa20da3ddf466980eebac3486c1e2299d5400303264bf4b7421e0cdf6e7f", size = 23813, upload-time = "2026-05-02T09:10:16.791Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bb/4aa60f78e9af97b2982501d67b936be1141c1f457c5fbee68d9c32368516/microsoft_agents_copilotstudio_client-1.1.0.dev7-py3-none-any.whl", hash = "sha256:d9f78523cf60d835c905dad708851463fa8408986a80480b46a9eb77960c931f", size = 23819, upload-time = "2026-06-16T09:14:07.405Z" }, ] [[package]] name = "microsoft-agents-hosting-core" -version = "0.10.0.dev4" +version = "1.1.0.dev7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -2070,9 +2075,9 @@ dependencies = [ { name = "pyjwt" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/b4/4a412794e6e2885f15a9b0ad9c59a3e062fbbe7851daced6b62cdfec546c/microsoft_agents_hosting_core-0.10.0.dev4.tar.gz", hash = "sha256:0e7689fb05d7b2fe7757e5953c83bbe242506f1ad6a5f224d8c7e4f94f598e90", size = 119529, upload-time = "2026-05-02T09:10:07.806Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/9f/1ef6f4134a3136fb746dafbe1b293b15e3e4ffc5b200c8ba7930a77ebc80/microsoft_agents_hosting_core-1.1.0.dev7.tar.gz", hash = "sha256:add663fa8d23faf57f7d3bba9c3d699f075969eb958934d5b3c170b6c79d8a11", size = 123424, upload-time = "2026-06-16T09:13:58.152Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/eb/7bc9ed8a60f8bade4ca08d050598e68a65fe7850ea454047c0ff622c9749/microsoft_agents_hosting_core-0.10.0.dev4-py3-none-any.whl", hash = "sha256:a449bfd3498c67150eed33faf0f6e2cb1ba5cf011de2c3459e6e079b85896366", size = 182042, upload-time = "2026-05-02T09:10:18.866Z" }, + { url = "https://files.pythonhosted.org/packages/61/e5/a38313f7edaddd5b156e825efd0c9eeb4ab660d40c30365effd4c0ed545a/microsoft_agents_hosting_core-1.1.0.dev7-py3-none-any.whl", hash = "sha256:f17316fdd9260655a40faf80c66e22f444dff831f15aa91df4027b031b1d2224", size = 186993, upload-time = "2026-06-16T09:14:09.426Z" }, ] [[package]] @@ -2113,25 +2118,25 @@ wheels = [ [[package]] name = "more-itertools" -version = "11.0.2" +version = "11.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] [[package]] name = "msal" -version = "1.36.0" +version = "1.38.0rc1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/cb/b02b0f748ac668922364ccb3c3bff5b71628a05f5adfec2ba2a5c3031483/msal-1.36.0.tar.gz", hash = "sha256:3f6a4af2b036b476a4215111c4297b4e6e236ed186cd804faefba23e4990978b", size = 174217, upload-time = "2026-04-09T10:20:33.525Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/ba/afc0474f72674e1a19155afe7b6b3a8b12359d2aee458e216f4dd8030f5b/msal-1.38.0rc1.tar.gz", hash = "sha256:c0160b98217f84705339189d0fa5099cdec0ffa5986e3d2053f450007a2f1a89", size = 182430, upload-time = "2026-06-05T15:20:47.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/d3/414d1f0a5f6f4fe5313c2b002c54e78a3332970feb3f5fed14237aa17064/msal-1.36.0-py3-none-any.whl", hash = "sha256:36ecac30e2ff4322d956029aabce3c82301c29f0acb1ad89b94edcabb0e58ec4", size = 121547, upload-time = "2026-04-09T10:20:32.336Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9f/873565789342901574aa85d228c6ed6761addf9f5a4829217e7f94671700/msal-1.38.0rc1-py3-none-any.whl", hash = "sha256:4c3528a473d856f725c8f9b302c364c576e21b5cf43a581273c2682d973c4da3", size = 123766, upload-time = "2026-06-05T15:20:48.981Z" }, ] [[package]] @@ -2272,63 +2277,53 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, - { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, - { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, - { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, - { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, - { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, - { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, - { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, - { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, - { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, - { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, - { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, - { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, - { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, - { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, - { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, - { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, - { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, - { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, - { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, - { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, - { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, - { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, - { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, - { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, - { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, - { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, - { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, - { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, - { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, - { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, - { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, - { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +version = "2.5.0rc1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/e1/f2fe8f6113a657a1da83b358b16755e528a602c6976a43724a88f42b482a/numpy-2.5.0rc1.tar.gz", hash = "sha256:4de3e0d48e58ef53e32c756bddea2722b9dde7ec25f96afc5fe7ece620cc4cb9", size = 20610894, upload-time = "2026-06-02T12:58:09.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/d5/39a7bac854c1c228d62e48535f431b6901159a1e054b39d13da363fe5fd4/numpy-2.5.0rc1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dc5f26f2155b2a64938adec26cd4652660e34588612cb08fe5e65860e638235a", size = 16790758, upload-time = "2026-06-02T12:56:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/da/a2/ede0d2a76e84f0a35796ecd3a2c8f9973b35bdc947ca3a395f3c7a72a157/numpy-2.5.0rc1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:859c145cf7ebe885a0c579a80bccfed9b35ceb6bbc23131149b118919764f407", size = 14807438, upload-time = "2026-06-02T12:56:26.229Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9a/c5b3472279f4c363c3919830a8005073d4cd6fb73816f1f9f8f5345405a6/numpy-2.5.0rc1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9cca573cd3610227420f3045a90bb5fd414f536043be16d8e46c8229830d244d", size = 5313467, upload-time = "2026-06-02T12:56:28.336Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/aecf9566e752f8ee0c16367932ef061fdacdfcc02b87e13ecefa6a163a1b/numpy-2.5.0rc1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:5f75ef317f45dae8090124630254b842b3cc10eea7db4f7f1a688f37cd285d2f", size = 6650465, upload-time = "2026-06-02T12:56:30.116Z" }, + { url = "https://files.pythonhosted.org/packages/94/d7/dad93b29f68ccc2310f3a071212eb2a60bd05b42f2bd50a3b807dcfa0323/numpy-2.5.0rc1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1586c886dfff6d814541ddedfa1317c4133ad2f171b89c8578b08f1d103a57c", size = 15153866, upload-time = "2026-06-02T12:56:32.355Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ea/0c9ebeceb3ab32585946799eb2ec86ff55d6b39d310e814c44855bd1d07f/numpy-2.5.0rc1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1763247152f2c929cf21767dfa36b2d3d91c304218e0751b79849859d595f50", size = 16661583, upload-time = "2026-06-02T12:56:34.798Z" }, + { url = "https://files.pythonhosted.org/packages/cd/74/a2d32f679cbfe23c7202613c73a654be1db4cee933bc332ab835ad3de1ef/numpy-2.5.0rc1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ddfeee2c7a587838c578fb999aba1523fac7cea2802ec24b25c8eb8fec2214ac", size = 16513414, upload-time = "2026-06-02T12:56:37.596Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c7/935e9164ad43852ade3b8cee0340dc20fe461f1682b9071cc4e2b76acff6/numpy-2.5.0rc1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5417326ce746c188be90e817b3471e97f11616ae2ac7d190aa3c128971b9b93a", size = 18417787, upload-time = "2026-06-02T12:56:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/12/1a/bb52a27340e606d0b78e17bae36c11bf59c070f758889b2cacea984b58e0/numpy-2.5.0rc1-cp312-cp312-win32.whl", hash = "sha256:99cf48d26b3341361d3d70fc7490af1499de41d2dba28a721a835acc83cb2d87", size = 6053661, upload-time = "2026-06-02T12:56:42.511Z" }, + { url = "https://files.pythonhosted.org/packages/47/70/c75cd23d7b49821dec7f0c5ae29e858d3f686f83b0b4ecfdc4c7a39db80a/numpy-2.5.0rc1-cp312-cp312-win_amd64.whl", hash = "sha256:e4afd8387b6617e61ecb0ceb60080c342366767b046fcc5c80516f19442f269b", size = 12416233, upload-time = "2026-06-02T12:56:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/c0ab9acddf4a4ca6fe77236938a9b85ad5227efaf628fc6e39b750276146/numpy-2.5.0rc1-cp312-cp312-win_arm64.whl", hash = "sha256:c072a4e5cad53345ab6ee29ca5b1a44df3cbc38991d066af71e6246583294e32", size = 10334234, upload-time = "2026-06-02T12:56:46.728Z" }, + { url = "https://files.pythonhosted.org/packages/f9/2c/6735b782a68595ba9cd6f453d5cef8e637b6ee89af3555e8074e5084f188/numpy-2.5.0rc1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:342294d39fbaaf93d33c8950d6b4fb873034e5eabbb67676190dd79c07ba10af", size = 16785923, upload-time = "2026-06-02T12:56:49.319Z" }, + { url = "https://files.pythonhosted.org/packages/00/24/1864bcfea4adb5fbffdb8063ae7993f537cb168fda1cfadcec3eb1afc940/numpy-2.5.0rc1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d3b9d87c617fbd05b9afb38cacd56a9f60e2d3c0dd96a7278028a07a1362e948", size = 14802138, upload-time = "2026-06-02T12:56:51.792Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/c15966ab370698ca66c8a64ac86172cbba3b62e9355f51c5b7f4c11a5840/numpy-2.5.0rc1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6b29138613d32227696eaad490d136cc114c2b3742cd379509d69cb97c013939", size = 5308122, upload-time = "2026-06-02T12:56:54.318Z" }, + { url = "https://files.pythonhosted.org/packages/63/46/ab26a2d69fe6babd2883c1d4eead5dfba869525b239e3152c02ea7a92cc7/numpy-2.5.0rc1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:8954bbb79e6ebc60e218b97168603a02475745b557ee0749f36da131e553fffd", size = 6645557, upload-time = "2026-06-02T12:56:55.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/a8/e6265c8deacae8445a591186baa507da578eebbb8799a64230119fbdc18a/numpy-2.5.0rc1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a8bdefbd3b2c8ec3e060f0aeccbd7db4d10658de34fa9c161304a876d4b7a57f", size = 15149536, upload-time = "2026-06-02T12:56:58.154Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cb/38e09c2d8d8af874ae9650d51ba74fb65af5e016e10154454e4f585699d7/numpy-2.5.0rc1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a4c5bccef100aad3ca67f0c82fec6c9c41d93f45af082765a49248af9e4aa4c", size = 16648175, upload-time = "2026-06-02T12:57:00.909Z" }, + { url = "https://files.pythonhosted.org/packages/18/b4/74ad66aab93cf2e45b01ffc00b552d988d2d3c7978f3b6f90415adf4529f/numpy-2.5.0rc1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fe949230df0a1c10e75e4ddbd0b382f1773c04ff8e4b988b43a3386b80885dd5", size = 16510274, upload-time = "2026-06-02T12:57:03.32Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/9025469bac3f2e11e327058e94e872b08d3e8eb22f9985ae20a05f46edbc/numpy-2.5.0rc1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9352b974bded4f5e82dd94c028207bdc26c5051c9aa76508215c8430a9f98047", size = 18406033, upload-time = "2026-06-02T12:57:05.872Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c8/e19a496ae871db9627f16c52ab7ba5393a2404dccefcc4c295c60b593da6/numpy-2.5.0rc1-cp313-cp313-win32.whl", hash = "sha256:79316ee5595e364c589a7d4b910006a818a2f8455e13ca1b544144d3b26b5c16", size = 6051769, upload-time = "2026-06-02T12:57:08.151Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/899d0ae5ca1c12af50c84a251b3a9807326ac6f2549289d3318649b60f1c/numpy-2.5.0rc1-cp313-cp313-win_amd64.whl", hash = "sha256:009be5174f1d8ebcc2f246eb1606c3e695a864f7d79821f3be5d7e2758213753", size = 12411388, upload-time = "2026-06-02T12:57:10.115Z" }, + { url = "https://files.pythonhosted.org/packages/db/31/aad462df2fda6948de95329dc1cb1d7f45651e51407937266f132b6a3e98/numpy-2.5.0rc1-cp313-cp313-win_arm64.whl", hash = "sha256:cc5330a6613229cc2511dd36578a0eb853e0aa6c248cf8d0a880bd775b8000e3", size = 10334033, upload-time = "2026-06-02T12:57:12.785Z" }, + { url = "https://files.pythonhosted.org/packages/d7/26/defbaa994ef5a2626a5c0d22a4d6e7c9f53c4699a3a534f03abbaa1cb1c0/numpy-2.5.0rc1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d672d8b3414f94d170dd4903b594c44bec4ae5ac1fcf37fe61e7cb0eb94f510d", size = 16784532, upload-time = "2026-06-02T12:57:15.2Z" }, + { url = "https://files.pythonhosted.org/packages/77/93/cac22c6d06e9a7220a1c167dd6cae45658460d8843f7b38c58673d9c4896/numpy-2.5.0rc1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a123a7f1ee049548835eea5af9e10f31ee1b334215b45b3fcc4a6f9240faa9e4", size = 14813009, upload-time = "2026-06-02T12:57:17.78Z" }, + { url = "https://files.pythonhosted.org/packages/56/69/2f4fa0a9deb91de9d4b5384e19b396a4bbfb9de38a30134a07e5e59aa8f5/numpy-2.5.0rc1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2461850c9e41a1abd79fa8538b52bc7a828a8f5565398d3f9a7bf2da4e891fdd", size = 5319042, upload-time = "2026-06-02T12:57:20.088Z" }, + { url = "https://files.pythonhosted.org/packages/f0/11/3448e6923d5e342e08d3929878472791d2e04ed4b348959a994d6b4f936b/numpy-2.5.0rc1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f6c70078f019426adda42de7ed2369b7631ed41ee2cec7064b2f90db23ac6507", size = 6642057, upload-time = "2026-06-02T12:57:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/70/5b/c2885366e1e4c68f4c5683bedb4caf7f868a80b813269ae4e157b30b9b53/numpy-2.5.0rc1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00a7bcd1be5d066f7ff834706f2ffb6aee7ebbb544236b5293e5c702a5dc22b7", size = 15167044, upload-time = "2026-06-02T12:57:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/8b877f5e097518319cc6c9dbb78c0dfed378109cf719b94dfa0a39d4d386/numpy-2.5.0rc1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:799814f0986b8fd3275b125b8efb1f066b3a9d1454d753e70648f38fb488bf0d", size = 16652160, upload-time = "2026-06-02T12:57:27.068Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d2/c8997aa8d92f599afec8b5c9d8e5f11a847cb894eee98a91cdde00b694f1/numpy-2.5.0rc1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4642001be298cc2b47d567feb499d4aa9a9b88dca795d6f147ceb16ebd685ff0", size = 16523955, upload-time = "2026-06-02T12:57:29.865Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5f/07de8d32301763b19a88fc8b63b0175971de3879537d0f501433bd736067/numpy-2.5.0rc1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19592531defb5237022cd60120b4aea1639065cd05cdf0bde731a2ed96eff6ab", size = 18410190, upload-time = "2026-06-02T12:57:32.767Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0c/ed1012f7dc187de21f21b634161f9661c702c713973aaf339463009d4495/numpy-2.5.0rc1-cp314-cp314-win32.whl", hash = "sha256:dacc0758f3b01f15fbc7a0e57d868d5ba55d19fc2fd19a979b8f8d00cb5e13d4", size = 6104044, upload-time = "2026-06-02T12:57:35.423Z" }, + { url = "https://files.pythonhosted.org/packages/b6/04/e24e16f1dde491350c64e82a37317da5f8f264d129ed4f9b5f117d4a4dfc/numpy-2.5.0rc1-cp314-cp314-win_amd64.whl", hash = "sha256:6089d453e0e6828f918636677fbceca9a82674b32f1fb327f9b1ee494ea36688", size = 12547661, upload-time = "2026-06-02T12:57:37.571Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d3/0b9bb44def2b4dbd6c962e30db3048a77c1ef3e8a3c1d9d57e95102b125b/numpy-2.5.0rc1-cp314-cp314-win_arm64.whl", hash = "sha256:185b5534a5e7d1d63742fefa30fe023f6372c2dc501a7944ab751598e3d1e0e5", size = 10611115, upload-time = "2026-06-02T12:57:39.982Z" }, + { url = "https://files.pythonhosted.org/packages/32/85/2c2d9798cf5ece4a2ea2fdfa6446e829c7038924885432643e26cd539d58/numpy-2.5.0rc1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b042e0c0e565785f7a949238c6f133b79b81798916764802c4fe6e42ec44152", size = 14929474, upload-time = "2026-06-02T12:57:42.506Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7c/63e362acb28a1829897d6ebddf5ff0b574296c33ba8c1ae8afe5b688c546/numpy-2.5.0rc1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:d6f5f960cf81af1bab473ae447b9bc0eeddfe75daa5c6d40666a06c64c85b935", size = 5435431, upload-time = "2026-06-02T12:57:44.77Z" }, + { url = "https://files.pythonhosted.org/packages/3d/65/a50dd4c4b95bafe57cd578d2c80ef28e6f9b961c138eff6482147cf7e2c0/numpy-2.5.0rc1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09d9c6728008dcfe9b5fafb78000a1349176d095e25abae7828026eb8922a3ab", size = 6745844, upload-time = "2026-06-02T12:57:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/e1e6dde63fa7b7df6bebd97c5f2ec91e918c58a264423338ae1ce2fa06dd/numpy-2.5.0rc1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a9e7396a878d06b74c3114753091178981cc1ed40de0afe924bed6961e5d53b", size = 15212366, upload-time = "2026-06-02T12:57:48.877Z" }, + { url = "https://files.pythonhosted.org/packages/85/20/f11412d706cc3622a55a4a5d04bcc6ac1a77c935cb5771c5629a6304b799/numpy-2.5.0rc1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b13412a84ce0b894c63148a68ab743b7f405f696dad6a7e72d9534f535137bd8", size = 16688617, upload-time = "2026-06-02T12:57:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/01/c7/4002b397c8ff3fc1c0109ddb9f5b9cc0c10a16f1627e96c63d8a8fba83d4/numpy-2.5.0rc1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5b32736a68a67c0935a678e92a798209f3acbf857306cfe1a807142cb7a6c7c9", size = 16577833, upload-time = "2026-06-02T12:57:54.23Z" }, + { url = "https://files.pythonhosted.org/packages/14/3e/3316e3b6db7505df3b8f66e74d5e102be4b9297a5661ba0f95df74991e96/numpy-2.5.0rc1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:28865f569d8c0caf71a3de85c95fd1f021aedb732bdd21bb1e37ed3a17644672", size = 18451445, upload-time = "2026-06-02T12:57:56.986Z" }, + { url = "https://files.pythonhosted.org/packages/60/27/dca162784210d5046a0e83128614d07e42e0196c240e32679b55d65cb3a0/numpy-2.5.0rc1-cp314-cp314t-win32.whl", hash = "sha256:a06ddccbcc74b3bdc63a8f781af8792b4dcb2bec9a39c0ff3c630372e638a1c7", size = 6251446, upload-time = "2026-06-02T12:57:59.788Z" }, + { url = "https://files.pythonhosted.org/packages/60/b1/bb7e49625673e0398d3ec6d1a7a7a46ba044569c8b6373d58d9931d1b823/numpy-2.5.0rc1-cp314-cp314t-win_amd64.whl", hash = "sha256:6a9405558adc50738a254e68c786467f7ab74a523a2689c51fb003863b8289c7", size = 12732397, upload-time = "2026-06-02T12:58:01.809Z" }, + { url = "https://files.pythonhosted.org/packages/53/dd/bec9b508b770988aa3218de36847f31b0bab94699e83d9d3d401379718df/numpy-2.5.0rc1-cp314-cp314t-win_arm64.whl", hash = "sha256:e2857303f8c8bfe897addc98099dd166a192fe847dc5147a5d7e49625143edcf", size = 10680555, upload-time = "2026-06-02T12:58:06.407Z" }, ] [[package]] @@ -2374,7 +2369,7 @@ wheels = [ [[package]] name = "openai-agents" -version = "0.15.1" +version = "0.17.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffelib" }, @@ -2386,14 +2381,14 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/52/125891e56b67ec78bef08d91dc0a8d39457088cd0f59bf8e74a37e5e591c/openai_agents-0.15.1.tar.gz", hash = "sha256:78c3f1226e1d6d34dd7566e211c8345e996629d1704335153d1728995a3a7775", size = 5319915, upload-time = "2026-05-02T02:20:53.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/16/b79c1849125eb6d19cae98c21ff35caa2e55b5ec8d7a02b354b711917ef7/openai_agents-0.17.3.tar.gz", hash = "sha256:63b6dda6bd4fb51169e2a2cbd5d187a4e5ce823bbd15f965c8ed1d3b89072eec", size = 5406135, upload-time = "2026-05-19T01:28:15.971Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/7b/69a33975b3610300219e1d25185c46280814cdb87e69a779acf0c9b9166a/openai_agents-0.15.1-py3-none-any.whl", hash = "sha256:2d304a5dcb919bc4fa1de5c7c9c93a4353a8a79d87d582d6170929390b7b3cef", size = 818627, upload-time = "2026-05-02T02:20:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/80/ec/775a14cfd5f12f4ffe458c7ac9527831093c72e8c1aef682898fc6394106/openai_agents-0.17.3-py3-none-any.whl", hash = "sha256:a048bb0752d40913d18bccf6562f56260b603bb57c972597b6da58f60123f4bd", size = 841541, upload-time = "2026-05-19T01:28:13.334Z" }, ] [[package]] name = "openai-chatkit" -version = "1.6.3" +version = "1.6.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -2402,9 +2397,9 @@ dependencies = [ { name = "pydantic" }, { name = "uvicorn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/46/b15fd77f7df12a2cabd8475de6226ce04d1cec7b283b21e8f0f52edc63a7/openai_chatkit-1.6.3.tar.gz", hash = "sha256:f16e347f39c376a78dddb5ceaf5398a4bb700c0145bfa7cb899d65135972956e", size = 61822, upload-time = "2026-03-04T19:30:19.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/07/c4b4ea034f34f25e73cf1a872deb349e3acab6c929a5531d547a9a994890/openai_chatkit-1.6.5.tar.gz", hash = "sha256:903e9702bf26cd8a2b23d4e7b199b657bee4379758e0ca11ebaee09362d2889e", size = 65057, upload-time = "2026-05-19T05:05:14.954Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/5e/e06a4bec431083c282dea5729b0947b940900a4014216835182048078877/openai_chatkit-1.6.3-py3-none-any.whl", hash = "sha256:642ecdf810eda3619964f316e393f252741130a5500dc3a357d501f8657b3941", size = 42578, upload-time = "2026-03-04T19:30:18.314Z" }, + { url = "https://files.pythonhosted.org/packages/aa/00/64b0faae946885e9bf52a8ea77d8bd01c80f6d0be967a30c1d4e73fc2ce1/openai_chatkit-1.6.5-py3-none-any.whl", hash = "sha256:9e16d3bdc6c15fec900801591dc5bb8ba9446447d3db2247dc8120518f1ca3c0", size = 44094, upload-time = "2026-05-19T05:05:13.654Z" }, ] [[package]] @@ -2708,20 +2703,20 @@ wheels = [ [[package]] name = "pathable" -version = "0.5.0" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/f3/5a20387de9bcd0607871bfc2198ee0e15836da7baa4592ccd7f24c27c986/pathable-0.6.0.tar.gz", hash = "sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58", size = 18970, upload-time = "2026-05-19T18:15:11.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e8/6d75ffd9784bce2e93d1ae4415649427e39a53bb172d4672b2b59c6f0a7b/pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566", size = 18983, upload-time = "2026-05-19T18:15:10.728Z" }, ] [[package]] name = "platformdirs" -version = "4.9.6" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] @@ -2747,7 +2742,7 @@ wheels = [ [[package]] name = "posthog" -version = "7.14.0" +version = "7.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff" }, @@ -2755,9 +2750,9 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/0f/0e6578feaf0d4e670bc517b6da09ec147a65421c44e0cd687eba12f08743/posthog-7.14.0.tar.gz", hash = "sha256:3be5e513f07e4ee5119f98b0458cb640739b49cef7c96c3e18b1d65076b18239", size = 205083, upload-time = "2026-05-01T20:41:37.971Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/24/c4880740bb875c05da8cb4cb70fcb4d0e572748bae30347f73de97217851/posthog-7.19.2.tar.gz", hash = "sha256:c39d464b5b5930bf930329ba2593a778d44ecf8724f2c3b473209e3e68ef6cad", size = 239161, upload-time = "2026-06-17T14:07:10.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/c2/2dc3e08e481f45c0215da4325ccc9b5f368dcee504779d2f169ab567c766/posthog-7.14.0-py3-none-any.whl", hash = "sha256:76db6e3158e2c11ec9bbcf32a673efec4acc8078965d92e2d3055555220ee546", size = 240187, upload-time = "2026-05-01T20:41:36.022Z" }, + { url = "https://files.pythonhosted.org/packages/3a/55/71d3e8ad027e4e4dca6dd2a21e233b2fe6de569822f17bb7e14860879903/posthog-7.19.2-py3-none-any.whl", hash = "sha256:1a8da398b684e328f67f4305ddd89d2b190285c34a34d954934cdb9fb4857996", size = 279371, upload-time = "2026-06-17T14:07:08.441Z" }, ] [[package]] @@ -2765,8 +2760,8 @@ name = "powerfx" version = "0.0.34" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "python_full_version < '3.14'" }, - { name = "pythonnet", marker = "python_full_version < '3.14'" }, + { name = "cffi" }, + { name = "pythonnet" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" } wheels = [ @@ -2864,98 +2859,108 @@ dev = [ [[package]] name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] name = "proto-plus" -version = "1.27.2" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/56/e647b0c675392d2da368da7b6f158f7368b18542fd6f7d7400a2f39de000/proto_plus-1.28.0.tar.gz", hash = "sha256:38e5696342835b08fc116f30a25665b29531cda9d5d5643e9b81fc312385abd9", size = 57221, upload-time = "2026-05-07T08:04:50.811Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl", hash = "sha256:a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8", size = 50410, upload-time = "2026-05-07T08:03:31.962Z" }, ] [[package]] @@ -3003,15 +3008,15 @@ wheels = [ [[package]] name = "py-key-value-aio" -version = "0.4.4" +version = "0.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beartype" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/e2/d689d922894a7ecde73b6daeaf9b13dab5aae06fe6aaaf7514722644d382/py_key_value_aio-0.4.5.tar.gz", hash = "sha256:c6563a2c6abe5da5e20f4f9e875c2a9b425a2244a54fadbf46cf140a9eea45d7", size = 107547, upload-time = "2026-05-27T16:37:08.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, + { url = "https://files.pythonhosted.org/packages/f6/95/b8ba862968712caa12a19666175334fa979e1f198b896a430adb3bacfe87/py_key_value_aio-0.4.5-py3-none-any.whl", hash = "sha256:ab862adbcb8c72547d1c57821f22cbbb71ab86509039c96f36e914e0336c8dd7", size = 170005, upload-time = "2026-05-27T16:37:06.629Z" }, ] [package.optional-dependencies] @@ -3058,7 +3063,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.3" +version = "2.14.0a1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -3066,9 +3071,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/71/0ae6d4e0a84faf0cd050033416509c44a19dc6dbec5bfdd3e1318cb1feb4/pydantic-2.14.0a1.tar.gz", hash = "sha256:2c3a5627d48f59725564c41b582328a29fa8d9b1108128ef6710463a0136d4fd", size = 844232, upload-time = "2026-05-22T13:23:43.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/91/4e/15d81754895da3f256273432da60fcb7c885177fefaf495c5b4364fadca5/pydantic-2.14.0a1-py3-none-any.whl", hash = "sha256:61a1ea8d65df95b681c1fab9cd7d01b2472837f798df53dc6d0f41f0c217b061", size = 470439, upload-time = "2026-05-22T13:23:40.57Z" }, ] [package.optional-dependencies] @@ -3078,91 +3083,92 @@ email = [ [[package]] name = "pydantic-core" -version = "2.46.3" +version = "2.47.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, - { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, - { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, - { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, - { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, - { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, - { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, - { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, - { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, - { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, - { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, - { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, - { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, - { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, - { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, - { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, - { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, - { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, - { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, - { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, - { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, - { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, - { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, - { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, - { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, - { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, - { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, - { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, - { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, - { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, - { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, - { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, - { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, - { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, - { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, - { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, - { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, - { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, - { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, - { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, - { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, - { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a7/74/319859f70c733f341df03823c8ca27ce9003faaac3ffa3110f3af1c8641a/pydantic_core-2.47.0.tar.gz", hash = "sha256:422c1797a7864b2a9a996435aba92fe571fb80190f67a31edbc1ac040c7b51fe", size = 476601, upload-time = "2026-05-22T13:19:00.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/23/2daeff7346433ad9a3567852dd7a716329f9a7952dcf1ee74b166271bdc7/pydantic_core-2.47.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:70a7aeba54854f5d97da65cb1a61f000f53df3704cab41cd81d65ab127ddd031", size = 2108216, upload-time = "2026-05-22T13:19:31.898Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/2989cb5112b892b7dc13af570ff57d0f383f770fc88bbb644262df1b3017/pydantic_core-2.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0e97329e38228f57fe1f2d91ba0ef39cc75cc1a84fe6ef58942d2fc6cb406bf", size = 1951502, upload-time = "2026-05-22T13:19:54.702Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a6/2417d8b1856cf7579a21e2679f02417b910e4f29120303e2c45137525072/pydantic_core-2.47.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:faf83e50714837af72f13e9369c50377552a4a74049d4477bed51c7e5822d94b", size = 1975590, upload-time = "2026-05-22T13:17:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/fc/71/0cd0813355b385bef8fa9a719982e0b44847afedc043b988c9f7c5d02ca9/pydantic_core-2.47.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f50abe60347bf8afe2b2f58db86cf3ac6e418eec7ffa01d9dc90ba29fc64f243", size = 2055885, upload-time = "2026-05-22T13:20:42.378Z" }, + { url = "https://files.pythonhosted.org/packages/a3/33/5687c6dcf8ecca106c95b2a49f04a633320ab49961da6e57682db37b4482/pydantic_core-2.47.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f13b18e7dec056336f29ee77dea3cc5db0271d6215cac7249cc5c61b0a49d293", size = 2235292, upload-time = "2026-05-22T13:19:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/36/98/08e81f1a0f0aa82cfa1a4d07b66eb116740fd6c82ba4baa8f96d4b377132/pydantic_core-2.47.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3aaabbdbbbb8dff33fa053ffb2c980f39dec745fc03592f50e1e010449129841", size = 2308945, upload-time = "2026-05-22T13:18:38.113Z" }, + { url = "https://files.pythonhosted.org/packages/04/f1/6e35bdbdb79f96e630265297452d709b2ebd5facfbd83d3eb702eee24939/pydantic_core-2.47.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d2907ee6d15cf26787bfbeb4c42e18e52f358086eab91baea961a0d909248d6", size = 2093861, upload-time = "2026-05-22T13:20:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c4/6a48e5da6a567bcf6d3ab113d2d476e1b93e3be941b4c486918f1c9d6714/pydantic_core-2.47.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:c413761260967f4dbb51135e1b49f30a1c29e15bf371fbb39754ed6475739545", size = 2124948, upload-time = "2026-05-22T13:18:15.728Z" }, + { url = "https://files.pythonhosted.org/packages/94/a1/e356c98dfc6bd5e6e166f2c832865a62941370b17a7199d35bc5999fecc8/pydantic_core-2.47.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bb527ac6e5a9721023b24615e1a55c01f47c60007f08b7d2afb89ff9c7a0e22", size = 2182065, upload-time = "2026-05-22T13:19:07.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/aa/6e99aac0a891b05cc522cc464aebeec2bb877fa4744be641a013ebc1785a/pydantic_core-2.47.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b3abf7fd5e6abe483a63413f9cd26b7c93c20780e19c8556434c7279f6b2f10c", size = 2185830, upload-time = "2026-05-22T13:18:34.551Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/c6c5c1ad40b4d58f11d6e754e9c3990a2a3cfff20a9cf5c1f0a855a791b2/pydantic_core-2.47.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:9cefe43d17a5a273d71697c084d3787defa7f578cd5fab4cef4c66d13c9e44b2", size = 2327330, upload-time = "2026-05-22T13:19:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f8/064747538297e385a1f197f35551d1bf6093e88c5ce9b4e15c035f0a1cfe/pydantic_core-2.47.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:38a8b5371b7938e4f6c31060dbd51d3b3229aef7c43eaac2eb8b153e43c3f189", size = 2366505, upload-time = "2026-05-22T13:20:11.895Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d5/2d8715ef982c0b1875e42f4248b48439133a2781f0be77f0e4dbc84d147d/pydantic_core-2.47.0-cp312-cp312-win32.whl", hash = "sha256:b87e95e644df2a36bd631dd0d6e097aa73d19a55adf7b1724ebdeece3d9c76b7", size = 1957410, upload-time = "2026-05-22T13:17:38.025Z" }, + { url = "https://files.pythonhosted.org/packages/0e/ea/9d908a80798db41c7c38926259ed74d015869f0afe4be6ed56f71793a084/pydantic_core-2.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0078c5695322050ccedbc86eafaf3e2548439782c51d99e575de0e31b9fe4f4", size = 2072354, upload-time = "2026-05-22T13:17:42.779Z" }, + { url = "https://files.pythonhosted.org/packages/fe/78/5549cba47c662ec7f05d79527474786e5ff7f08ef3e65a409a8a2ab6013e/pydantic_core-2.47.0-cp312-cp312-win_arm64.whl", hash = "sha256:7eedc31996e9eba3bdfbbc380805ac6d765c889b7e93b17cd00ecb0200fd6dca", size = 2035452, upload-time = "2026-05-22T13:18:18.75Z" }, + { url = "https://files.pythonhosted.org/packages/7a/91/810cabda42f7fdd13b349702694e1140f6071fd42d1e542d606d5ad0c2d4/pydantic_core-2.47.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:263560ece98bffbbc0a8047ce60b8a278c859db6a2a4e30d9454b02891045eca", size = 2108548, upload-time = "2026-05-22T13:18:09.878Z" }, + { url = "https://files.pythonhosted.org/packages/fa/87/f444a6d63bcbdff3c45291df842941ed56c568f45dd3742f25afb567b5a2/pydantic_core-2.47.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a7fdaff39a66bf66e9037da482575513d2f20bfb02ea9d9222b5cb3b902fc695", size = 1951421, upload-time = "2026-05-22T13:20:32.973Z" }, + { url = "https://files.pythonhosted.org/packages/f5/09/c90dff5407c11e59023161c84df19dd5ed93966a23b2f08cd21d45812ab9/pydantic_core-2.47.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3e6c8ee5ce8c270bfae09763ae4bbbccfe81090c97d670a621fb86cb1ef6042", size = 1976554, upload-time = "2026-05-22T13:19:04.085Z" }, + { url = "https://files.pythonhosted.org/packages/95/cf/55fbe9f1b396f91005cd1cb7acf2bcf380a1f1b57e261ac78bcdc0ff42f3/pydantic_core-2.47.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e38cdae682cfec4b3816722dccf6376ca59049726d57dca83c2fe7cc13665589", size = 2055005, upload-time = "2026-05-22T13:18:12.809Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c6/4f7cbaa62582e95c080cdd45fb5d4350ff05dac87ea32930c75dcb427bf1/pydantic_core-2.47.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc3193ff0b7e2e168f84c6185e70475738c191f3154e0af8f897cd0f8f9a489", size = 2235489, upload-time = "2026-05-22T13:19:13.994Z" }, + { url = "https://files.pythonhosted.org/packages/31/75/16913c82ffd194c537222b3d0e8a05c11681e551add67308ec4af6023724/pydantic_core-2.47.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57ff41672a615f38af528ee904602be51c653248354e5db8e9252668abe91e68", size = 2308360, upload-time = "2026-05-22T13:18:39.894Z" }, + { url = "https://files.pythonhosted.org/packages/05/9f/b24bb1b764fc360adace5df806a81fd62ef1662df2973891e487c3fd5a2c/pydantic_core-2.47.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473b9a2b2a1f0dd55cbb32d2b902f93babe7f141a0bb48fb4d3d4d2b3e93e9a0", size = 2092549, upload-time = "2026-05-22T13:17:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d3/0268ea5972b91178250c0a573bb54c17f697665cd2aa35623087a3589fc6/pydantic_core-2.47.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:195f9c4ac43a7b2a044a7b86631c3352abdb820bed2823ea29f98f779255f459", size = 2123849, upload-time = "2026-05-22T13:18:03.817Z" }, + { url = "https://files.pythonhosted.org/packages/8f/92/26b2147738a89f78925775bb4b34ae53f981ce880ee77ee2c2ccbc49466a/pydantic_core-2.47.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c1cd10d39ef1ff8bcd68b6865bee9c434631ac0608d402fe86e678851c2e2a5", size = 2181733, upload-time = "2026-05-22T13:17:35.308Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fa/8a858dde4784c7245b7216ab9a71518cb1a898a83ffb5c0509181789de19/pydantic_core-2.47.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7cbe66352fe2b39511d49150e5b52159429cd21f5633a3e801dd2c43829dcdca", size = 2184065, upload-time = "2026-05-22T13:18:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/69/20/ea165877f965622e021f04d63330f9ceb55ede0aefa862863e06a9a610cb/pydantic_core-2.47.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e35192a1d53e55d510d8bb1023c988c7cdae6d94539074971741b2a7656e49a1", size = 2326881, upload-time = "2026-05-22T13:18:41.477Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d6/ad22a6f0941be5a5478e6c378d0ec3c7641be96d7a86a3ea1f9e9830a269/pydantic_core-2.47.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:81de54576de2e20baec76cc5afae2820f9049e6fdc4f357bac3391da02d0ba97", size = 2365574, upload-time = "2026-05-22T13:20:02.947Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4e/eb5e3429dd29311e75ab89313cde09b709b0637fd88838d2cb4a98760b02/pydantic_core-2.47.0-cp313-cp313-win32.whl", hash = "sha256:05da6647bdfd3888936ac10aa39b239d659f3c93dff281af0fc5943eb55629dd", size = 1955451, upload-time = "2026-05-22T13:17:31.178Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/ec107c12aa8729c766285d4aa6caa5f5addf8b2ef6cfd35c455a3ca5c66e/pydantic_core-2.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:021220e0a03b66112737ee1fc49759340ce8fafb8d9ade1b7fb366b06033fa45", size = 2071285, upload-time = "2026-05-22T13:19:50.585Z" }, + { url = "https://files.pythonhosted.org/packages/f4/02/b003a55acfeb35823925d1cd502b80e3fe6c5d477865c36815fbefdf49ac/pydantic_core-2.47.0-cp313-cp313-win_arm64.whl", hash = "sha256:55156ee2f6f561ea4e25ab55f84bd70b9c9ed2546a834cb2b038fe10225aaa37", size = 2034804, upload-time = "2026-05-22T13:18:32.64Z" }, + { url = "https://files.pythonhosted.org/packages/ef/46/a4675c783822e229bf04e290ae28cf5a057b00a46039498743b066e0fefd/pydantic_core-2.47.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6e37a6974fbd8fa7cae12285a76970d50b3689ffd6ed7c7fdd176ba81dd22d0e", size = 2104537, upload-time = "2026-05-22T13:18:05.312Z" }, + { url = "https://files.pythonhosted.org/packages/67/bb/f6d6d1dd8362d616b227b55af8bc2d1b7d4ea842facb7c50a82298ba3b9d/pydantic_core-2.47.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2433b8524785cc117e602233bc574879bc8d87f09523edeec51665d5c46cf42d", size = 1951581, upload-time = "2026-05-22T13:18:14.244Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/7d01ed8658ccf8beae8a3fcf2cd023c3ba0ab0b19d3f456845a709cece94/pydantic_core-2.47.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f9f2be064c8bf1189f46f7062fd42765d94f59cfb7db7ef8db19563192110a", size = 1978461, upload-time = "2026-05-22T13:19:15.829Z" }, + { url = "https://files.pythonhosted.org/packages/80/e5/dd38ccbd1e68f1c3fd7f8b413a8b32db11bd16d5999c3dc3aed4e54290df/pydantic_core-2.47.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1f0a9659a2eb161573418e3138f616101ba21bbd2ff04916dca7b6712155e015", size = 2049444, upload-time = "2026-05-22T13:19:06.258Z" }, + { url = "https://files.pythonhosted.org/packages/7d/63/0cb998f3f4ea4255ab6d891e2537d4566cd31630df0d406ce45b00306729/pydantic_core-2.47.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2995074b99242aa28991e0120a3c881babc139e08750a05b7ea7d140644e091d", size = 2231772, upload-time = "2026-05-22T13:20:05.22Z" }, + { url = "https://files.pythonhosted.org/packages/3f/20/2cacf5c4a1bdda1356351df38f343c6cf13af6194e6e0ee0f7967395793f/pydantic_core-2.47.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c5b224dc04c3ff9b08c24419464eb7f6ad7a1049e12284a00bf80df82bd15fdb", size = 2305322, upload-time = "2026-05-22T13:20:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8f/b7e09086bd48b1ef3f74227ed98907496924a622c7f9315cf661946233c6/pydantic_core-2.47.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:264361b7236d4374fef6342908f87d084a0d58a2f8d0811e99f714309cb0ba7e", size = 2097748, upload-time = "2026-05-22T13:20:21.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/27/2927049e93f6fbbaa838b2bb656dbd63c6d6fcaab9909d632fd93573ebd9/pydantic_core-2.47.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:53368beaf693f6302a6e33bdefe950857534a04d282811421bd20176d0fb5636", size = 2122971, upload-time = "2026-05-22T13:19:37.977Z" }, + { url = "https://files.pythonhosted.org/packages/f2/3b/80b58a0c7f4339f024ad5c5b6316bef895536abf75c45dc85f0c83ae1a92/pydantic_core-2.47.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d2177b44ba7d9d86850f865f362feeaac6a2ed8517a9b505b97ff0b7fdbd7dd", size = 2181287, upload-time = "2026-05-22T13:18:20.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/0a/dfcb9575603aaf113d2cae1fb622570046a210276eba2267764b3f83f4f2/pydantic_core-2.47.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fb57a6b538ec7a01b937986bc093aec530fb056135b6bc9cfdd0bf8460c25bc2", size = 2177842, upload-time = "2026-05-22T13:17:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/57/c2/31a198c8180b417d18410e7640c505b39c51ac291aca34f71ac37093f4c4/pydantic_core-2.47.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:18c9c7c3a18e9bdbf1215d913f6bd00e17595dc92949817935cb87a3cf5f1697", size = 2321802, upload-time = "2026-05-22T13:17:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/052d872b61f88d2f71b3d881c08abfbe33cb9d64bd988712bb4fb973001c/pydantic_core-2.47.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2f72a382886ca85bb1247303b9134cf9978c9d454de62e710a1ebcd9d2131927", size = 2363870, upload-time = "2026-05-22T13:18:45.053Z" }, + { url = "https://files.pythonhosted.org/packages/be/60/c8c985d4081dafaa88fe6ac6a41ebe2b82d289c391b2bcbd3508f585e7a9/pydantic_core-2.47.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:cdf4dc2cdd0eacad1bd81c4d25422b4c25b206acae095d2d64e5d5cb7facc6b3", size = 1369335, upload-time = "2026-05-22T13:17:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/42/70/30dfcc18dc5552f7235ae2ffd7b699a1ba0d38ebfcdb97f371c792d61601/pydantic_core-2.47.0-cp314-cp314-win32.whl", hash = "sha256:1e859dd5e06e9807080e14995db131649a77c61131cc464a7fe492a69ce82488", size = 1952107, upload-time = "2026-05-22T13:20:24.07Z" }, + { url = "https://files.pythonhosted.org/packages/fa/49/f5c517ba99b9e1036ead16cedd0fc189dbcbf900fb0b85b746b6a0b9b389/pydantic_core-2.47.0-cp314-cp314-win_amd64.whl", hash = "sha256:234ecade0e358caa1ea516c218b3f61e61e30532cad1a8bb12f2487325838548", size = 2071388, upload-time = "2026-05-22T13:18:46.74Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6c/6b95e00ed7a3c7f78cb8544bc34cb29412c05512af507fc4466912576d50/pydantic_core-2.47.0-cp314-cp314-win_arm64.whl", hash = "sha256:58158d0111e86893bc35aacabe509f951ed303cddf8cdba43533190bde317914", size = 2026046, upload-time = "2026-05-22T13:18:22.112Z" }, + { url = "https://files.pythonhosted.org/packages/22/2c/fa67f96f381ba3c83e8cd75a6eb3c538e123d8d02e19d80383bff01ffda0/pydantic_core-2.47.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:24017fd3befd4d7cc4a8c71f4a1e9a44d29fdc91723c5446b0e795ab808adee7", size = 2102407, upload-time = "2026-05-22T13:17:46.882Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/99abe6c33db3dda235a2095eae5fdd03266857abd9be1c50ed97a59587c3/pydantic_core-2.47.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dfa0820888cc4549fcce7e6bd8affa7d75198d885ccd0bb0760def4bb8461862", size = 1931963, upload-time = "2026-05-22T13:18:23.776Z" }, + { url = "https://files.pythonhosted.org/packages/5c/dd/96d1884f65737f793e64b6b8745ee793e3cb4735e086ead0a40e5349294e/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1d507f331756f7066cde7c9f35ed78fa78223a54369dbc8a34d6da7a5074fa1", size = 1973492, upload-time = "2026-05-22T13:17:22.128Z" }, + { url = "https://files.pythonhosted.org/packages/07/8c/0504f091c75b229cd07d61b45464d7c5291c3ecf2e7d847f215fe81cf5e5/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c4a885fb50c05903bc703a00830616e680304fdcdd90fc9535a52e72debe712", size = 2035548, upload-time = "2026-05-22T13:17:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bf/e9f69c55ef6c945d29e4afb306f62a15354bfd09f1ee25d9aecd4c6cbb82/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b107cbd764bf68f12a57c7aa5846d868bc7463490a1ac2d0f19bffef624c5a9", size = 2238356, upload-time = "2026-05-22T13:19:10.309Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/dfbd214487033093f5946c1fd5915ae5dc6b278efe764397b5e2ef8092a8/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b5f46412c8226d0c8f1f423c324c75afe342e4b854836933579fb484f68598c", size = 2284799, upload-time = "2026-05-22T13:17:27.959Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ec/f80488fa26a6b27bd0d1267f35b0fce3bfeec23ca97021085a94ccd3adc3/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90cbf7e35d597503cbdb5cd85409cbb75f377290bc7e8e37cc5dfe4f5cc66cf8", size = 2107895, upload-time = "2026-05-22T13:18:27.282Z" }, + { url = "https://files.pythonhosted.org/packages/9b/1d/67ac210745f63a982b145d8f281dc76bd347d98052792f087224a178da54/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:04ee7ba7172cb4484af51b2890f19069d35773698abc8c6ebb651f52fbf41134", size = 2102797, upload-time = "2026-05-22T13:19:40.139Z" }, + { url = "https://files.pythonhosted.org/packages/6c/75/f3b9bd4d88fcaad9a5abf2ec6969f37a70e34f40a5f8d2d78077f168e83d/pydantic_core-2.47.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a7acb3e120ddba94372b8146e62ea3a0bce203180e34641c817c87f995c91e0", size = 2160123, upload-time = "2026-05-22T13:19:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/e94ac13bd17c341e86c4b67deb47b63718208fe413034fdb01e7a4bf1dd8/pydantic_core-2.47.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:1a2f7ceb58013d167d8c96f10ec9b3a137018c819ba356f68ff1cb74302fd22e", size = 2164386, upload-time = "2026-05-22T13:18:48.372Z" }, + { url = "https://files.pythonhosted.org/packages/36/e5/862ff195402f5b08db9ad4d9ebe7cbed993f51528aab7edcb62531442556/pydantic_core-2.47.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:482b097637fec5037eb13fe9f9d5fe47e568a5b451d686bc2b854076cc0b50ca", size = 2303767, upload-time = "2026-05-22T13:18:50.211Z" }, + { url = "https://files.pythonhosted.org/packages/c2/05/c8ebbb97cf44686fc9cc64a6bb86ac72a8871426a9f61417e395d77c4894/pydantic_core-2.47.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a8c7f5fad73eb404f4b84c75f3d9d3865b748ded248b7366341db6e516fc502b", size = 2361148, upload-time = "2026-05-22T13:18:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/4695ec6ce81eda7cc822383de993c5b2c289cfc84d3f032a30c55eb238fc/pydantic_core-2.47.0-cp314-cp314t-win32.whl", hash = "sha256:f343c39928097175acf2f7d0cba5c00b0f62265d88a173a4ce264266ac849bd9", size = 1939588, upload-time = "2026-05-22T13:19:42.151Z" }, + { url = "https://files.pythonhosted.org/packages/96/4d/bc2e188ab648b3ab284e80a0340c5d5b9723c22572c6e36ba39eb300e718/pydantic_core-2.47.0-cp314-cp314t-win_amd64.whl", hash = "sha256:52d40e074da44e42b2425aedebd513e405a31807036ef597175117a9b01743a6", size = 2049697, upload-time = "2026-05-22T13:18:08.244Z" }, + { url = "https://files.pythonhosted.org/packages/6f/46/ca1e6813e029993e57340c6a37740450c51919e3809472f79060f4d64cf0/pydantic_core-2.47.0-cp314-cp314t-win_arm64.whl", hash = "sha256:25cac08c9735e61e5c0b9f7a85c438661fc0e9da226afdfa984c3da6c5942a5a", size = 2025906, upload-time = "2026-05-22T13:17:50.322Z" }, + { url = "https://files.pythonhosted.org/packages/4e/21/81cc7e3acbf7d4eed41e9585e81b5840aaf6ddbe65974a20946038d6516b/pydantic_core-2.47.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:d648515c65f8871ceca6a0446be32fb1a0aae414db3907ec0df6cc2380dd0c04", size = 2098016, upload-time = "2026-05-22T13:18:56.902Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6e/c98e869e9f754f18088ad9b55887972d08606e7bf81dba088779e927eb65/pydantic_core-2.47.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3292813ac8ed17671a5b1ac8256b6e98b96c29c1c6d061c35899e2f6e0444cac", size = 1931175, upload-time = "2026-05-22T13:20:07.566Z" }, + { url = "https://files.pythonhosted.org/packages/5d/65/ce0d773f4b51dc332764ba1bd585c81f62d5c92231a66d152aff16be8dfa/pydantic_core-2.47.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf3cfcea028f41a9b42a47f4a53d72ca4274d04e3ee525bd58ca89ea8c5e1910", size = 1995356, upload-time = "2026-05-22T13:19:44.143Z" }, + { url = "https://files.pythonhosted.org/packages/19/2c/6e63683a77d342c93bad67bb0762ca8fe1d8418acc8617d70ccdcc5ec5ff/pydantic_core-2.47.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52afdd8640d59890539060e91c8a494cf96b463e63b2ba499cc5c23abcb5e96b", size = 2144893, upload-time = "2026-05-22T13:17:59.008Z" }, ] [[package]] name = "pydantic-settings" -version = "2.14.0" +version = "2.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, + { 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" }, ] [[package]] @@ -3176,11 +3182,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] @@ -3278,15 +3284,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.2.2" +version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, ] [[package]] @@ -3300,11 +3306,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.27" +version = "0.0.32" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] [[package]] @@ -3321,7 +3327,7 @@ name = "pythonnet" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "clr-loader", marker = "python_full_version < '3.14'" }, + { name = "clr-loader" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" } wheels = [ @@ -3339,18 +3345,21 @@ wheels = [ [[package]] name = "pywin32" -version = "311" +version = "312" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] [[package]] @@ -3410,7 +3419,7 @@ wheels = [ [[package]] name = "qdrant-client" -version = "1.17.1" +version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "grpcio" }, @@ -3421,23 +3430,23 @@ dependencies = [ { name = "pydantic" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/dd/f8a8261b83946af3cd65943c93c4f83e044f01184e8525404989d22a81a5/qdrant_client-1.17.1.tar.gz", hash = "sha256:22f990bbd63485ed97ba551a4c498181fcb723f71dcab5d6e4e43fe1050a2bc0", size = 344979, upload-time = "2026-03-13T17:13:44.678Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/45/5b1bdd15a3c7730eefb9c113600829e20d689b82b5a23f9e07d107094004/qdrant_client-1.18.0.tar.gz", hash = "sha256:52e8ece1a7d40519801bf0b70713bfa0f6b7ae28c7275bbe0b0286fbed7f6db4", size = 352580, upload-time = "2026-05-11T14:12:38.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/69/77d1a971c4b933e8c79403e99bcbb790463da5e48333cc4fd5d412c63c98/qdrant_client-1.17.1-py3-none-any.whl", hash = "sha256:6cda4064adfeaf211c751f3fbc00edbbdb499850918c7aff4855a9a759d56cbd", size = 389947, upload-time = "2026-03-13T17:13:43.156Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/c437bd2ac41ef30d3019063e6ce537dc111e9214473b337ee88f7fa6359a/qdrant_client-1.18.0-py3-none-any.whl", hash = "sha256:093aa8cf8a420ee3ad2a68b007e1378d7992b2600e0b53c193fc172674f659cd", size = 398126, upload-time = "2026-05-11T14:12:36.998Z" }, ] [[package]] name = "redis" -version = "7.4.0" +version = "7.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad", size = 4943913, upload-time = "2026-03-24T09:14:37.53Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/93/05e7d4a65285066a74f48697f9b9cde5cfce71398033d69ed83c3d98f5c9/redis-7.4.1.tar.gz", hash = "sha256:1a1df5067062cf7cbe677994e391f8ee0840f499d370f1a71266e0dd3aa9308e", size = 4945742, upload-time = "2026-06-05T09:10:06.703Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2e/2677f3f93dae0497e7e33b6637302e7f3744efc553f34231183e32584885/redis-7.4.1-py3-none-any.whl", hash = "sha256:1fa4647af1c5e93a2c685aa248ee44cce092691146d41390518dabe9a99839b0", size = 410171, upload-time = "2026-06-05T09:10:05.128Z" }, ] [[package]] name = "redisvl" -version = "0.18.1" +version = "0.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpath-ng" }, @@ -3449,9 +3458,9 @@ dependencies = [ { name = "redis" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/db/7d89a51e3571d3e0dba285a8b784ead25f0653c05a8f4f29515fb62e2e2a/redisvl-0.18.1.tar.gz", hash = "sha256:2beaf7dfe7c6412fe116759823f5e1eba5cdf8719b6b5c482cf0e62f8abeeb6b", size = 885584, upload-time = "2026-04-30T16:25:55.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/4c/2b4845f3e47ffcb7d220d83d3ec84aa044426119e71e6fefbd9c581ce7f8/redisvl-0.20.1.tar.gz", hash = "sha256:58c316e5cba75669a9fedfb783a2c02cbb93b20d11026291b80c2b78c82a2a45", size = 1110436, upload-time = "2026-06-10T13:57:50.627Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/04/18ba1697feb5fe0a4638a95de05193e9c3ed15912bd02c20dcfb8063d28c/redisvl-0.18.1-py3-none-any.whl", hash = "sha256:e6dd9c1fa4d8dd2b27f04bd86545ba55a2c712b99e75e90ec031ef498932d67a", size = 225554, upload-time = "2026-04-30T16:25:54.181Z" }, + { url = "https://files.pythonhosted.org/packages/99/88/8b0830f796c7abf65c6aaa075a9b45abfeae1d1ba13c432dd0e0768a1185/redisvl-0.20.1-py3-none-any.whl", hash = "sha256:b89f3fe412ee2167d475d2303db1563acee5156e4e89515d92d83759696b77be", size = 320529, upload-time = "2026-06-10T13:57:49.279Z" }, ] [[package]] @@ -3470,7 +3479,7 @@ wheels = [ [[package]] name = "requests" -version = "2.34.0.dev1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -3478,9 +3487,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/37/b3032e92a7712e988c92df2ed408d6aec5b00838e6c06009ae695433915b/requests-2.34.0.dev1.tar.gz", hash = "sha256:319ba4e42f1031737a08f3efc695c7dc436f22efb8d02630ca3a99cf23f752cd", size = 141686, upload-time = "2026-05-03T20:21:41.747Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/53/ddb8b8fa96367976cf52bb0610ffd529bd7d2795b2e4c1724724d071718c/requests-2.34.0.dev1-py3-none-any.whl", hash = "sha256:c8749aeb3c4b204f80fd288f7507378c9afe66a3f189fb43fd77ea33e74d7564", size = 73077, upload-time = "2026-05-03T20:21:40.509Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -3496,27 +3505,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, ] -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, -] - -[[package]] -name = "rfc3986" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, -] - [[package]] name = "rich" version = "15.0.0" @@ -3532,83 +3520,112 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +version = "2026.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" }, + { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" }, + { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" }, + { url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" }, + { url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" }, + { url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, + { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, + { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, + { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, + { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, + { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, + { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, + { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, + { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, + { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, + { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, + { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, + { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, + { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, + { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, + { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, + { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, + { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, + { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, ] [[package]] @@ -3741,28 +3758,28 @@ wheels = [ [[package]] name = "sse-starlette" -version = "3.4.1" +version = "3.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/9a/f35932a8c0eb6b2287b66fa65a0321df8c84e4e355a659c1841a37c39fdb/sse_starlette-3.4.1.tar.gz", hash = "sha256:f780bebcf6c8997fe514e3bd8e8c648d8284976b391c8bed0bcb1f611632b555", size = 35127, upload-time = "2026-04-26T13:32:32.292Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/07/45c21ed03d708c477367305726b89919b020a3a2a01f72aaf5ad941caf35/sse_starlette-3.4.1-py3-none-any.whl", hash = "sha256:6b43cf21f1d574d582a6e1b0cfbde1c94dc86a32a701a7168c99c4475c6bd1d0", size = 16487, upload-time = "2026-04-26T13:32:30.819Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, ] [[package]] name = "starlette" -version = "1.0.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/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +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/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, + { 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]] @@ -3776,26 +3793,26 @@ wheels = [ [[package]] name = "tqdm" -version = "4.67.3" +version = "4.68.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, ] [[package]] name = "types-requests" -version = "2.33.0.20260503" +version = "2.33.0.20260518" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/b8/57e94268c0d82ac3eaa2fc35aa8ca7bbc2542f726b67dcf90b0b00a3b14d/types_requests-2.33.0.20260503.tar.gz", hash = "sha256:9721b2d9dbee7131f2fb39f20f0ebb1999c18cef4b512c9a7932f3722de7c5f4", size = 23931, upload-time = "2026-05-03T05:20:08.882Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/82/959113a6351f3ca046cd0a8cd2cee071d7ea47473560557a01eeae9a6fe2/types_requests-2.33.0.20260503-py3-none-any.whl", hash = "sha256:02aaa7e3577a13471715bb1bddb693cc985ea514f754b503bf033e6a09a3e528", size = 20736, upload-time = "2026-05-03T05:20:07.858Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" }, ] [[package]] @@ -3821,11 +3838,11 @@ wheels = [ [[package]] name = "uncalled-for" -version = "0.3.1" +version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/68/35c1d87e608940badbcfeb630347aa0509897284684f61fab6423d02b253/uncalled_for-0.3.1.tar.gz", hash = "sha256:5e412ac6708f04b56bef5867b5dcf6690ebce4eb7316058d9c50787492bb4bca", size = 49693, upload-time = "2026-04-07T13:05:06.462Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/82/345cc927f7fbdae6065e7768759932fcc827fc20b29b45dfbafa2f1f7da4/uncalled_for-0.3.2.tar.gz", hash = "sha256:89f5dbcd71e2b8f47c030b1fa302e6cce2ec795d1ac565eeb6525c5fe55cb8a2", size = 50032, upload-time = "2026-05-06T13:38:25.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/e1/7ec67882ad8fc9f86384bef6421fa252c9cbe5744f8df6ce77afc9eca1f5/uncalled_for-0.3.1-py3-none-any.whl", hash = "sha256:074cdc92da8356278f93d0ded6f2a66dd883dbecaf9bc89437646ee2289cc200", size = 11361, upload-time = "2026-04-07T13:05:05.341Z" }, + { url = "https://files.pythonhosted.org/packages/3b/25/2c87754f3a9e692315f7b811244090e68f362979fc8886b3fbd2985a1d8c/uncalled_for-0.3.2-py3-none-any.whl", hash = "sha256:0ff60b142c7d1f8070bde9d42afaa70aedc77dcc10998c227687e9c15713418e", size = 11444, upload-time = "2026-05-06T13:38:24.025Z" }, ] [[package]] @@ -3839,15 +3856,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.46.0" +version = "0.49.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" }, ] [package.optional-dependencies] @@ -3895,7 +3912,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.3.1" +version = "21.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -3903,79 +3920,95 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" }, + { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" }, ] [[package]] name = "watchfiles" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, ] [[package]] @@ -4101,113 +4134,91 @@ wheels = [ [[package]] name = "yarl" -version = "1.23.0" +version = "1.24.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, + { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, + { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, + { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, + { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ] [[package]] name = "zipp" -version = "3.23.1" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, ] From ce729c1a423260b910200176e696d6ed677b075d Mon Sep 17 00:00:00 2001 From: llPriyanka <40491221+llPriyanka@users.noreply.github.com> Date: Fri, 19 Jun 2026 05:47:42 +0000 Subject: [PATCH 48/50] conflict resolve --- src/backend-api/uv.lock | 215 ++++++++++++++++++++-------------------- src/processor/uv.lock | 42 ++++---- 2 files changed, 129 insertions(+), 128 deletions(-) diff --git a/src/backend-api/uv.lock b/src/backend-api/uv.lock index 0d18f3df..611f3f74 100644 --- a/src/backend-api/uv.lock +++ b/src/backend-api/uv.lock @@ -2,9 +2,10 @@ version = 1 revision = 3 requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version == '3.13.*'", - "python_full_version < '3.13'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", ] [manifest] @@ -254,15 +255,15 @@ wheels = [ [[package]] name = "anyio" -version = "4.13.0" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, ] [[package]] @@ -487,7 +488,7 @@ wheels = [ [[package]] name = "azure-monitor-opentelemetry-exporter" -version = "1.0.0b52" +version = "1.0.0b53" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -497,9 +498,9 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "psutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/7e/bfc03436b88c48f5adc21a3ebbf4392b6b7fbbfe33ef3b1e88d07ba9f380/azure_monitor_opentelemetry_exporter-1.0.0b52.tar.gz", hash = "sha256:7eac679fca32dee9e426df65f2a538161db4514fc322fc66107f7826567d86e1", size = 326179, upload-time = "2026-05-11T22:47:02.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/64/875f13849fe2e3832ceda6a218fa5422a25e72c1b86623a8514f541a8c60/azure_monitor_opentelemetry_exporter-1.0.0b53.tar.gz", hash = "sha256:1274e9008909414a25c6287185a6c5a884209705b6e651a1ffddfbdab3b76e52", size = 335614, upload-time = "2026-06-08T15:54:22.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/e8/d13e6a74c98ecc3011bce9ab09fc2e75aec48ab46288f72be57c2fa21460/azure_monitor_opentelemetry_exporter-1.0.0b52-py2.py3-none-any.whl", hash = "sha256:a38c503e5e2cc0ec8a4bf336b23cce23488719f5361a45cdd01a514080f0e7fc", size = 244751, upload-time = "2026-05-11T22:47:04.304Z" }, + { url = "https://files.pythonhosted.org/packages/5f/dc/6dbff881ac739999bc31f166504ed31a2e0fef45cc59a3c1b2386a4b4abe/azure_monitor_opentelemetry_exporter-1.0.0b53-py2.py3-none-any.whl", hash = "sha256:e2faadaf203369f25cb96df2a75017bd7f11655b033200c5867151f8fb2a6ba7", size = 249226, upload-time = "2026-06-08T15:54:24.413Z" }, ] [[package]] @@ -549,11 +550,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.5.20" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] @@ -908,15 +909,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, ] -[[package]] -name = "detect-installer" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" }, -] - [[package]] name = "distro" version = "1.9.0" @@ -978,22 +970,21 @@ standard = [ [[package]] name = "fastapi-cli" -version = "0.0.24" +version = "0.0.27" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "rich-toolkit" }, { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/d0/ee5678346811967b8d096d5d5604e71b50d6bf5a2abfbdb331157e2bbaa9/fastapi_cli-0.0.27.tar.gz", hash = "sha256:1dffb1e40c0c88f2e0171a8a252a2b615c1e63ff8c05626649e4badd6a84336a", size = 23630, upload-time = "2026-06-18T14:48:43.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ab/0a709f9488fe62647db80f8a277fb0ee62e85adc6746abf477ed373c9eb7/fastapi_cli-0.0.27-py3-none-any.whl", hash = "sha256:2e389a40f318e29fec8cb1e289f267f17c048876fb82dbfa869a10b16740495d", size = 13070, upload-time = "2026-06-18T14:48:44.311Z" }, ] [package.optional-dependencies] standard = [ - { name = "fastapi-cloud-cli", version = "0.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, - { name = "fastapi-cloud-cli", version = "0.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "fastapi-cloud-cli" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -1001,48 +992,21 @@ standard = [ name = "fastapi-cloud-cli" version = "0.11.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14'", -] dependencies = [ - { name = "fastar", marker = "python_full_version >= '3.14'" }, - { name = "httpx", marker = "python_full_version >= '3.14'" }, - { name = "pydantic", extra = ["email"], marker = "python_full_version >= '3.14'" }, - { name = "rich-toolkit", marker = "python_full_version >= '3.14'" }, - { name = "rignore", marker = "python_full_version >= '3.14'" }, - { name = "sentry-sdk", marker = "python_full_version >= '3.14'" }, - { name = "typer", marker = "python_full_version >= '3.14'" }, - { name = "uvicorn", extra = ["standard"], marker = "python_full_version >= '3.14'" }, + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, ] sdist = { url = "https://files.pythonhosted.org/packages/11/15/6c3d85d63964340fde6f36cc80f3f365d35f371e6a918d68ff3a3d588ef2/fastapi_cloud_cli-0.11.0.tar.gz", hash = "sha256:ecc83a5db106be35af528eccb01aa9bced1d29783efd48c8c1c831cf111eea99", size = 36170, upload-time = "2026-01-15T09:51:33.681Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1a/07/60f79270a3320780be7e2ae8a1740cb98a692920b569ba420b97bcc6e175/fastapi_cloud_cli-0.11.0-py3-none-any.whl", hash = "sha256:76857b0f09d918acfcb50ade34682ba3b2079ca0c43fda10215de301f185a7f8", size = 26884, upload-time = "2026-01-15T09:51:34.471Z" }, ] -[[package]] -name = "fastapi-cloud-cli" -version = "0.19.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.13.*'", - "python_full_version < '3.13'", -] -dependencies = [ - { name = "detect-installer", marker = "python_full_version < '3.14'" }, - { name = "fastar", marker = "python_full_version < '3.14'" }, - { name = "httpx", marker = "python_full_version < '3.14'" }, - { name = "pydantic", extra = ["email"], marker = "python_full_version < '3.14'" }, - { name = "rich-toolkit", marker = "python_full_version < '3.14'" }, - { name = "rignore", marker = "python_full_version < '3.14'" }, - { name = "sentry-sdk", marker = "python_full_version < '3.14'" }, - { name = "typer", marker = "python_full_version < '3.14'" }, - { name = "uvicorn", extra = ["standard"], marker = "python_full_version < '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/7c/f194925af8fabdb0b7a886a1b89087c0b7f327f99e79497a882aa94c1e34/fastapi_cloud_cli-0.19.0.tar.gz", hash = "sha256:f97b31c2ad6af3832eb4065870bdca3365b6e827a0ccf6eeb15e477bc1662b13", size = 57476, upload-time = "2026-06-01T08:24:03.407Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/e6/1a2ec890fc273b9da2b173ca45f692a2e24a369bdd39ea7812c1d8a799e5/fastapi_cloud_cli-0.19.0-py3-none-any.whl", hash = "sha256:a2dfc4074c321e63ec88589cc1f90573d4b5bf980ddc44a7033e6f3cd8e96628", size = 38239, upload-time = "2026-06-01T08:24:02.437Z" }, -] - [[package]] name = "fastar" version = "0.11.0" @@ -1311,11 +1275,11 @@ wheels = [ [[package]] name = "idna" -version = "3.17" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] @@ -1594,27 +1558,60 @@ wheels = [ name = "mcp" version = "1.27.2" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", +] dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt" }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, + { name = "anyio", marker = "python_full_version >= '3.14'" }, + { name = "httpx", marker = "python_full_version >= '3.14'" }, + { name = "httpx-sse", marker = "python_full_version >= '3.14'" }, + { name = "jsonschema", marker = "python_full_version >= '3.14'" }, + { name = "pydantic", marker = "python_full_version >= '3.14'" }, + { name = "pydantic-settings", marker = "python_full_version >= '3.14'" }, + { name = "pyjwt", marker = "python_full_version >= '3.14'" }, + { name = "python-multipart", marker = "python_full_version >= '3.14'" }, + { name = "pywin32", marker = "python_full_version >= '3.14' and sys_platform == 'win32'" }, + { name = "sse-starlette", marker = "python_full_version >= '3.14'" }, + { name = "starlette", marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.14'" }, + { name = "uvicorn", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/27/3c/347cf965d313f5d41764e7d46bea6ffe7d9ef13b983cc429b0340962a082/mcp-1.27.2.tar.gz", hash = "sha256:8e02db104096d1c25b28e64bde29a5c32b31bc241710213e12fd4d84985bdfef", size = 621116, upload-time = "2026-05-29T17:16:04.039Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c9/11/252c6f971dc4f16af1d98a1c469d8ba523aab00d1bb76b4d3bc1ff32eacc/mcp-1.27.2-py3-none-any.whl", hash = "sha256:d6ff5160c6ca65d93013626efb3fc249de683c30b2d8570755ceddd490344de5", size = 220498, upload-time = "2026-05-29T17:16:02.442Z" }, ] +[[package]] +name = "mcp" +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.14'" }, + { name = "httpx", marker = "python_full_version < '3.14'" }, + { name = "httpx-sse", marker = "python_full_version < '3.14'" }, + { name = "jsonschema", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "pydantic-settings", marker = "python_full_version < '3.14'" }, + { name = "pyjwt", marker = "python_full_version < '3.14'" }, + { name = "python-multipart", marker = "python_full_version < '3.14'" }, + { name = "pywin32", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, + { name = "sse-starlette", marker = "python_full_version < '3.14'" }, + { name = "starlette", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, + { name = "typing-inspection", marker = "python_full_version < '3.14'" }, + { name = "uvicorn", marker = "python_full_version < '3.14' and sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/ee/94c6c50ffc5b5cf4737052275d11b57367f32d1a8516e31dcd60591b3916/mcp-1.28.0.tar.gz", hash = "sha256:559d3f9943674cafbe5744c5d3794f3237e8b47f9bbc58e20c0fad680d8487c2", size = 636040, upload-time = "2026-06-16T21:37:17.996Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/e1/4c1dc1fbb688641a712d34650c3d58bbbdcb314ddb75bc5817bbf33515a4/mcp-1.28.0-py3-none-any.whl", hash = "sha256:9c1e7cf3a9125557e418ecd4fed8e9adddce81b0dfdae4d6601d700f5beb71a4", size = 221959, upload-time = "2026-06-16T21:37:16.579Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -1855,7 +1852,7 @@ wheels = [ [[package]] name = "openai" -version = "2.40.0" +version = "2.43.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1867,9 +1864,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/9f/136562ec6c3b1a50fe06eb0bb34ed21f0d7426ec0140e5cc43ac785b69a5/openai-2.40.0.tar.gz", hash = "sha256:9a756f91f274a24ad6026cbcb2042fd356c8d4a10e8f347b08d34465e585f7a2", size = 781177, upload-time = "2026-06-01T21:48:23.878Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/fa/88d0c58a0c58df7e6758e66b99c5d028d5e0bb49f8812d7203940cd9dbf1/openai-2.43.0.tar.gz", hash = "sha256:e74d238200a26868977002190fb6631613480a93dfe0c9c982e77021ed60a017", size = 785369, upload-time = "2026-06-17T17:06:56.06Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/46/180e14be801a75bc13f234cb1b594b232adeb9c84e60a9ab1832e8333591/openai-2.40.0-py3-none-any.whl", hash = "sha256:2b205637ff214477f9ce9ab035e9f494db0e3fa8f1e599008953735fbf6ff1ff", size = 1350935, upload-time = "2026-06-01T21:48:21.462Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d2/ba767f4bbb30776c03d40906a2d3afad716a165ffa1771fc23b8992f7920/openai-2.43.0-py3-none-any.whl", hash = "sha256:65a670b54fadf2268c9e1330133373c963eb779ee969e5cbad419ec2c21dce97", size = 1355077, upload-time = "2026-06-17T17:06:53.614Z" }, ] [[package]] @@ -2584,7 +2581,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2593,9 +2590,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" }, ] [[package]] @@ -2645,18 +2642,21 @@ wheels = [ [[package]] name = "pywin32" -version = "311" +version = "312" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] [[package]] @@ -2774,16 +2774,16 @@ wheels = [ [[package]] name = "rich-toolkit" -version = "0.19.10" +version = "0.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/02/32217f3657ae91a0ea7cf1d74ade78f44352f830d00c468f753ddb3d4980/rich_toolkit-0.19.10.tar.gz", hash = "sha256:dc2e8c515ef9fbb4894e62bd41a2d2960dd7c2f505b5084894604d5ccfee3f09", size = 198167, upload-time = "2026-05-21T10:11:42.397Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/63/3e427c62f1992945c997d4ec31e2fcb37d26aadbe5aa44ae5b29f7f64d26/rich_toolkit-0.20.1.tar.gz", hash = "sha256:c7336ae281f435c785acecaedc4b71d4b663dc73d9c8079fea96372527e822a4", size = 203473, upload-time = "2026-06-05T08:56:57.679Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/84/a005adcb4d1e6846ba3d62768090c3b943e3f6d8dc5c47af64f33584c4a7/rich_toolkit-0.19.10-py3-none-any.whl", hash = "sha256:93a41f67a09aefe90379f1729495c2fee9ccbcc8cfda48e2ca2ae54a995e32b1", size = 33907, upload-time = "2026-05-21T10:11:43.578Z" }, + { url = "https://files.pythonhosted.org/packages/00/88/309f07d08155da2ba1d5ceb42d270fb42fbe34a807684543e3ffc10fe713/rich_toolkit-0.20.1-py3-none-any.whl", hash = "sha256:2a6d5f8e15759b9eba5a9ee63da10b275359ead20e5a0fc92bd5b4dbae8ce4bf", size = 35525, upload-time = "2026-06-05T08:56:58.586Z" }, ] [[package]] @@ -3065,7 +3065,8 @@ dependencies = [ { name = "cloudevents" }, { name = "defusedxml" }, { name = "jinja2" }, - { name = "mcp" }, + { name = "mcp", version = "1.27.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "mcp", version = "1.28.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, { name = "nest-asyncio" }, { name = "numpy" }, { name = "openai" }, @@ -3095,15 +3096,15 @@ azure = [ [[package]] name = "sentry-sdk" -version = "2.61.1" +version = "2.63.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/3b/4bc6b348bbd331daa14d4babe9f2b99bc854f4da41560eefb9488d78481d/sentry_sdk-2.61.1.tar.gz", hash = "sha256:9c6adccb3feefa9ba032c8d295ca477575c2f11896046a2b0ad686c47c4af555", size = 459429, upload-time = "2026-06-01T07:24:18.875Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/c8/b3c970a5b186722d276cd40a05b3254e03bccc0208560aff20f612e018e8/sentry_sdk-2.63.0.tar.gz", hash = "sha256:2a1502bf864769275dbc8c2c9fc7a0f7f5e18358180b615d262d13a31ffba216", size = 912449, upload-time = "2026-06-16T12:45:57.553Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/54/c9218db183846e08efaf68534889ef42e499dde432778881104a42f7071b/sentry_sdk-2.61.1-py3-none-any.whl", hash = "sha256:fa36eaf4b8ad708f718500d4bdcc1532637526a22beb874d88cbc0a46458b5ae", size = 483735, upload-time = "2026-06-01T07:24:17.027Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/cb205f7d93373120f666b9c5736dc0815524d96a9b278e7a728f018dc22a/sentry_sdk-2.63.0-py3-none-any.whl", hash = "sha256:3a9b5ddd403f79eb73bd670f75f04485819db53d28f76ced7bc09041cb0dfd6a", size = 495950, upload-time = "2026-06-16T12:45:55.819Z" }, ] [[package]] @@ -3161,19 +3162,19 @@ wheels = [ [[package]] name = "tqdm" -version = "4.67.3" +version = "4.68.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, ] [[package]] name = "typer" -version = "0.26.5" +version = "0.26.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -3181,9 +3182,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/1a/2cf40b65b1d9c254fe5814bb0519f9b8f2ac38059df0810f9b866300c04a/typer-0.26.5.tar.gz", hash = "sha256:9b9b39e35c3afc9e1e51a06f21155246e457c0911279b09b35d8210ca74b935c", size = 201494, upload-time = "2026-06-01T14:42:49.744Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/ed/ef06584ccdd5c410df0837951ecd7e15d9a6144ea1bd4c73cecab1a89891/typer-0.26.7.tar.gz", hash = "sha256:e314a34c617e419c091b2830dda3ea1f257134ff593061a8f5b9717ab8dddb3a", size = 201709, upload-time = "2026-06-03T07:18:06.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/d6/baac76fc04a6532883de3d8722c7f921dae94d10965e7ffba9e38e42a251/typer-0.26.5-py3-none-any.whl", hash = "sha256:4bfd901d564e41608920134aa5d4481200f4ba76d98e982d9f9d32dcb7b84da0", size = 122451, upload-time = "2026-06-01T14:42:51.021Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/2201973529af2c954de0bb725323c3aaed6d7f0ceee8f550dec9185df013/typer-0.26.7-py3-none-any.whl", hash = "sha256:5c87cfbc5d34491c5346ebf49c23e18d56ccb863268d3a8d592b26087c2f5e58", size = 122456, upload-time = "2026-06-03T07:18:05.732Z" }, ] [[package]] diff --git a/src/processor/uv.lock b/src/processor/uv.lock index f091bcda..f0bedcb4 100644 --- a/src/processor/uv.lock +++ b/src/processor/uv.lock @@ -485,7 +485,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.109.2" +version = "0.111.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -497,9 +497,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/b7/9a8e2f79011e89dd6eeb599c27332aed765dac9d6fbee3a55e68e4e3ec25/anthropic-0.109.2.tar.gz", hash = "sha256:d37db299597c7bc124b49b767ff135f1e6456b64af2b2fad4b63b2a1df333cf0", size = 927559, upload-time = "2026-06-15T17:30:25.024Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/8a/9afc7305a2ce4b52b30e137f83cd2a6a90b918b3997073db11bb5a1de55a/anthropic-0.111.0.tar.gz", hash = "sha256:39cbda0ac17a6d423e5bf609811bd69b26eddf6299d7a468126e05bc711ce826", size = 934001, upload-time = "2026-06-18T17:31:44.733Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/f2/bee5de8a2699fc8a3cce34d61c7a2626a2c310ddde7ea5611327eb0ddbe9/anthropic-0.109.2-py3-none-any.whl", hash = "sha256:e0fb4ca5df0ed983248c9c6c3242adc81d9cfddb8725902da53698554117abac", size = 923800, upload-time = "2026-06-15T17:30:23.124Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bb/09e82a81885d787f350fb55ca9df865b63140dd28b3b5b3104c4ae261657/anthropic-0.111.0-py3-none-any.whl", hash = "sha256:c14edb36ed80da9099acbd26b5cec810d76606c31f32a0d56a4cf9d4fa9e25ae", size = 929774, upload-time = "2026-06-18T17:31:43.116Z" }, ] [[package]] @@ -1206,7 +1206,7 @@ wheels = [ [[package]] name = "durabletask" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asyncio" }, @@ -1214,22 +1214,22 @@ dependencies = [ { name = "packaging" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/78/099bc761bec57fd8fdd906058bc7a9c1b8caf8a0bfff774c25162ef1a20c/durabletask-1.5.0.tar.gz", hash = "sha256:93a80f6fb04d8fdad90b0d69518d27a70947cceb5ae1c6fb6a286ff481d19c16", size = 112887, upload-time = "2026-06-05T22:36:51.031Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/ae/2a9ef2fc99d3103eee04106c8608796a98811168a81423d89224bdeec255/durabletask-1.6.0.tar.gz", hash = "sha256:15ad61e865f4977055430ddf1d72ad667f6b06b8f8b2eef3a37271720b1dfb2a", size = 142541, upload-time = "2026-06-18T19:53:08.797Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/67/c0a33b3208ad862eef23e1ddcc452b3759af0d24e73b91f3c517c43d863d/durabletask-1.5.0-py3-none-any.whl", hash = "sha256:58371340fe37714bf662d9c9d05791e038203e6b2e7888e7a4b9927af0999711", size = 125164, upload-time = "2026-06-05T22:36:52.259Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6c/35f9be7816e8f8fa19d2de49adef8735c072daafc0964713de2b14d0b59d/durabletask-1.6.0-py3-none-any.whl", hash = "sha256:c604c7d8b483fafa13c6762cc293314e4ce68c4ceff5683246abb97e190dd989", size = 163769, upload-time = "2026-06-18T19:53:09.95Z" }, ] [[package]] name = "durabletask-azuremanaged" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-identity" }, { name = "durabletask" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/53/653c4a4a120906cfff9c824583bd9398528657ba683bfe9d12daac064c94/durabletask_azuremanaged-1.5.0.tar.gz", hash = "sha256:5af9fabb67a555039087b5242d628587b5179a2cfa6609cfbc5b227a0e8669e6", size = 5944, upload-time = "2026-06-05T22:37:10.299Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/09/002e9a5f870e02a060a09d15b82af54676b4fe8636a5c624f9a92bc26168/durabletask_azuremanaged-1.6.0.tar.gz", hash = "sha256:1cfa06035f9a70902b43a2587aaef10f1c1b7735e5f336e6b3b97f2a5fd3805f", size = 18206, upload-time = "2026-06-18T19:52:53.097Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/22/b6b7173209f03b458fae9ae98d20b074bf0a1788286227489d9bd23df302/durabletask_azuremanaged-1.5.0-py3-none-any.whl", hash = "sha256:260249dc537010f01b12215f57072a2dbdf00892132aa37a543dcc66712502af", size = 8314, upload-time = "2026-06-05T22:37:11.247Z" }, + { url = "https://files.pythonhosted.org/packages/a5/bb/5e5385be3b640e79b6d170e392f374a4a3c363ea5165ddb2c170c528b27f/durabletask_azuremanaged-1.6.0-py3-none-any.whl", hash = "sha256:32262c736a41d718c72f030aa69e51887e59593da80a69fbae064359f15435f8", size = 25459, upload-time = "2026-06-18T19:52:54.087Z" }, ] [[package]] @@ -1637,11 +1637,11 @@ wheels = [ [[package]] name = "idna" -version = "3.18" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] @@ -2041,31 +2041,31 @@ wheels = [ [[package]] name = "microsoft-agents-activity" -version = "1.1.0.dev7" +version = "1.1.0.dev8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/19/776fa3a7f63470fe2ee3641ffa810ecf2fe15580f155114d59446cba0b76/microsoft_agents_activity-1.1.0.dev7.tar.gz", hash = "sha256:55ced3bc4f3df634de125723323f29e99cbf44481cac2d1e103d516cbc6c2e3a", size = 62799, upload-time = "2026-06-16T09:13:54.072Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/a2/5d6ce5230fa8194bb67425fc1a5655f21d28398b75b7f2266ea5ba164de7/microsoft_agents_activity-1.1.0.dev8.tar.gz", hash = "sha256:da6526e29860edb896bc155adc62d659b4decbd75955f07aa0c553a47dc14681", size = 62808, upload-time = "2026-06-18T11:36:42.32Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/ac/dcd98767a80e7040b087eefcef6724435d0e5bada7bc36e57056cfb6713e/microsoft_agents_activity-1.1.0.dev7-py3-none-any.whl", hash = "sha256:de4c76ea207113c85cafcdf6e0d4429342cdc17176bac3285280eb5b4164d031", size = 135004, upload-time = "2026-06-16T09:14:05.141Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2c/e621de1b4717f8cb4510a40fa547ef25002c43bccbdb39995fe43d13922d/microsoft_agents_activity-1.1.0.dev8-py3-none-any.whl", hash = "sha256:10b8c546342caf2b8062352057ec6599b2d99e58a62573e15f73734b0b188009", size = 135004, upload-time = "2026-06-18T11:36:53.817Z" }, ] [[package]] name = "microsoft-agents-copilotstudio-client" -version = "1.1.0.dev7" +version = "1.1.0.dev8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "microsoft-agents-hosting-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/0b/4ca87e9a390f0016981898964d289ff788dfcdd819a7d7f3e0da71284aab/microsoft_agents_copilotstudio_client-1.1.0.dev7.tar.gz", hash = "sha256:acfa7d0aa0034ead271306d6ee93619927f081b6e82ca0c5de54ec7c39895957", size = 27470, upload-time = "2026-06-16T09:13:56.262Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/de/bef67460292e393a103356ef23606bac8572f3fe82a90fbe0a0160e18713/microsoft_agents_copilotstudio_client-1.1.0.dev8.tar.gz", hash = "sha256:f447c73ab960c93830bcbeded661a99748283164446f776bbec1263347452acb", size = 27486, upload-time = "2026-06-18T11:36:44.488Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/bb/4aa60f78e9af97b2982501d67b936be1141c1f457c5fbee68d9c32368516/microsoft_agents_copilotstudio_client-1.1.0.dev7-py3-none-any.whl", hash = "sha256:d9f78523cf60d835c905dad708851463fa8408986a80480b46a9eb77960c931f", size = 23819, upload-time = "2026-06-16T09:14:07.405Z" }, + { url = "https://files.pythonhosted.org/packages/90/39/f268ef3b6a14804bd4f82789629f75268118b68932b6772e7b246c1b6ef4/microsoft_agents_copilotstudio_client-1.1.0.dev8-py3-none-any.whl", hash = "sha256:5bfc791e5aede902632cc4c95ce3b3f56c1ab90a547bbd5c67ce742ffc2754fb", size = 23819, upload-time = "2026-06-18T11:36:56.062Z" }, ] [[package]] name = "microsoft-agents-hosting-core" -version = "1.1.0.dev7" +version = "1.1.0.dev8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -2076,9 +2076,9 @@ dependencies = [ { name = "pyjwt" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/9f/1ef6f4134a3136fb746dafbe1b293b15e3e4ffc5b200c8ba7930a77ebc80/microsoft_agents_hosting_core-1.1.0.dev7.tar.gz", hash = "sha256:add663fa8d23faf57f7d3bba9c3d699f075969eb958934d5b3c170b6c79d8a11", size = 123424, upload-time = "2026-06-16T09:13:58.152Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/ef/9004f0f366ed5dfd6a05895fedd3b7b62addb2df8a57b441143cfb5a0d3e/microsoft_agents_hosting_core-1.1.0.dev8.tar.gz", hash = "sha256:0ea8735d6185ae686b8f29c170a582f8f1d4d9469bf33917920354992828aba2", size = 123386, upload-time = "2026-06-18T11:36:46.54Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/e5/a38313f7edaddd5b156e825efd0c9eeb4ab660d40c30365effd4c0ed545a/microsoft_agents_hosting_core-1.1.0.dev7-py3-none-any.whl", hash = "sha256:f17316fdd9260655a40faf80c66e22f444dff831f15aa91df4027b031b1d2224", size = 186993, upload-time = "2026-06-16T09:14:09.426Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ae/92e12d175d745f0561a34c96e5364966301f5aab7860a21a8a9ca931925b/microsoft_agents_hosting_core-1.1.0.dev8-py3-none-any.whl", hash = "sha256:f2fdb5d268b293931c17b66c7f402b8bc0fdc476dacb835ffbf6df0a4e2e5fe7", size = 186994, upload-time = "2026-06-18T11:36:58.14Z" }, ] [[package]] From 755bf8e3dfe7f25b9dab3a4429e4c4daf75b7b50 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Fri, 19 Jun 2026 15:52:15 +0530 Subject: [PATCH 49/50] Enhance token usage tracking and emission in Azure OpenAI clients and orchestrator - Updated `_try_emit_token_event` to emit token usage to App Insights. - Integrated token usage tracking in non-streaming and streaming responses. - Registered active token usage tracker in `GroupChatOrchestrator` for accurate context. - Added context management for active tracker and agent/step labels in `token_usage_tracker`. --- .../azure_openai_response_retry.py | 186 ++++++------------ .../agent_framework/groupchat_orchestrator.py | 23 ++- .../src/utils/token_usage_tracker.py | 104 ++++++++++ 3 files changed, 187 insertions(+), 126 deletions(-) diff --git a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py index e648d64b..18d0cf39 100644 --- a/src/processor/src/libs/agent_framework/azure_openai_response_retry.py +++ b/src/processor/src/libs/agent_framework/azure_openai_response_retry.py @@ -42,16 +42,44 @@ def _extract_tokens_from_dict_or_obj(ud: Any) -> tuple[int, int, int]: def _try_emit_token_event(inp: int, out: int, tot: int, source: str) -> None: - """Log token usage found in stream/response for diagnostics. - - The actual LLM_Token_Usage event is emitted by TokenUsageTracker.record() - in the orchestrator, which has full context (agent, step, model, user). - This function only logs for debugging to avoid duplicate events. + """Emit token usage found in a stream/response to App Insights. + + The chat client retry wrapper is the only layer that reliably sees raw LLM + ``usage_details``; the orchestrator's streaming events and final conversation + do not surface usage Content. We therefore record here, against the active + ``TokenUsageTracker`` (resolved from context), which both aggregates the + usage and emits the per-call ``LLM_*`` Application Insights events. When no + tracker is active in the current context this falls back to a diagnostic log. """ - if tot > 0 or inp > 0 or out > 0: - logger.info( - "[TOKEN_STREAM] usage found: input=%s output=%s total=%s source=%s", - inp, out, tot, source, + if not (tot > 0 or inp > 0 or out > 0): + return + + logger.info( + "[TOKEN_STREAM] usage found: input=%s output=%s total=%s source=%s", + inp, out, tot, source, + ) + try: + from utils.token_usage_tracker import record_usage_from_context + + emitted = record_usage_from_context( + input_tokens=int(inp), + output_tokens=int(out), + total_tokens=int(tot), + ) + if emitted: + logger.info( + "[TOKEN_STREAM] emitted LLM token event: total=%s source=%s", + tot, source, + ) + else: + logger.warning( + "[TOKEN_STREAM] no active TokenUsageTracker in context; " + "token usage NOT emitted (total=%s source=%s)", + tot, source, + ) + except Exception as e: + logger.warning( + "[TOKEN_STREAM] failed to emit token usage (source=%s): %s", source, e ) @@ -836,8 +864,8 @@ async def _non_streaming_with_retry( try: response = await _retry_call( - lambda: parent_inner_get_response( - messages=effective_messages, chat_options=chat_options, **kwargs + lambda: parent_inner( + messages=effective_messages, options=options, stream=False, **kwargs ), config=self._retry_config, ) @@ -891,12 +919,15 @@ async def _non_streaming_with_retry( "[AOAI_CTX_TRIM] sleeping %ss before retry", round(trim_delay, 1) ) await asyncio.sleep(trim_delay) - return await _retry_call( + response = await _retry_call( lambda: parent_inner( messages=trimmed, options=options, stream=False, **kwargs ), config=self._retry_config, ) + # Emit token usage from the retried (context-trimmed) response too. + _emit_usage_from_response(response) + return response class AzureOpenAIChatClientWithRetry(OpenAIChatCompletionClient): @@ -1040,123 +1071,29 @@ def _maybe_trim_messages( return trimmed return messages - iterator = stream.__aiter__() - try: - first = await iterator.__anext__() - - async def _tail(): - yield first - async for item in iterator: - yield item - - _item_count = 0 - _last_item = None - async for item in _tail(): - _item_count += 1 - _last_item = item - _emit_usage_from_stream_item(item) - yield item - - # After stream completes, log diagnostic about the last item - if _last_item is not None: - try: - _attrs = [a for a in dir(_last_item) if not a.startswith("_")] - _contents = getattr(_last_item, "contents", None) - _content_info = [] - if _contents: - for _c in _contents: - _ct = getattr(_c, "type", "?") - _ca = [a for a in dir(_c) if not a.startswith("_")] - _content_info.append({"type": _ct, "attrs": _ca}) - _usage_attr = getattr(_last_item, "usage", None) - logger.info( - "[TOKEN_DIAG_FINAL] stream_items=%d last_item_type=%s attrs=%s contents=%s usage_attr=%s", - _item_count, - type(_last_item).__name__, - _attrs, - _content_info, - repr(_usage_attr) if _usage_attr is not None else "None", - ) - except Exception: - pass - return - except StopAsyncIteration: - return - except Exception as e: - close = getattr(stream, "aclose", None) - if callable(close): - try: - await close() - except Exception: - logger.debug("Best-effort close of response stream failed", exc_info=True) - - # Progressive retry for context-length failures. - if ( - self._context_trim_config.enabled - and self._context_trim_config.retry_on_context_error - and _looks_like_context_length(e) - ): - # Make trimming progressively more aggressive on each retry - # GPT-5.1: 272K input tokens ≈ 800K chars. Scale down from 600K default. - scale = attempt_index + 1 - aggressive_cfg = ContextTrimConfig( - enabled=True, - max_total_chars=max( - 30_000, - self._context_trim_config.max_total_chars - scale * 100_000, - ), - max_message_chars=max( - 2_000, - self._context_trim_config.max_message_chars - scale * 8_000, - ), - keep_last_messages=max( - 4, - self._context_trim_config.keep_last_messages - scale * 8, - ), - keep_head_chars=max( - 500, - self._context_trim_config.keep_head_chars - scale * 3_000, - ), - keep_tail_chars=max( - 500, - self._context_trim_config.keep_tail_chars - scale * 1_000, - ), - keep_system_messages=True, - retry_on_context_error=True, - ) - trimmed = _trim_messages(effective_messages, cfg=aggressive_cfg) - logger.warning( - "[AOAI_CTX_TRIM_STREAM] retrying after context-length error (attempt %s); count=%s -> %s, budget=%s", - attempt_index + 1, - len(effective_messages), - len(trimmed), - aggressive_cfg.max_total_chars, - ) - effective_messages = trimmed - if attempt_index >= attempts - 1: - # No more retries available. - raise - - # Cool down before retrying — immediate retries after trimming - # tend to trigger 429s because the API hasn't recovered yet. - trim_delay = self._retry_config.base_delay_seconds * ( - 2**attempt_index - ) - trim_delay = min(trim_delay, self._retry_config.max_delay_seconds) - logger.info( - "[AOAI_CTX_TRIM_STREAM] sleeping %ss before retry", - round(trim_delay, 1), - ) - await asyncio.sleep(trim_delay) - continue + async def _non_streaming_with_retry( + self, + *, + effective_messages: MutableSequence[Any] | list[Any], + original_messages: MutableSequence[Any], + options: Any = None, + **kwargs: Any, + ) -> Any: + """Non-streaming path: full retry + context-trim fallback.""" + parent_inner = super( + AzureOpenAIChatClientWithRetry, self + )._inner_get_response try: - return await _retry_call( + response = await _retry_call( lambda: parent_inner( messages=effective_messages, options=options, stream=False, **kwargs ), config=self._retry_config, ) + # Extract and emit token usage from the non-streaming response. + _emit_usage_from_response(response) + return response except Exception as e: if not ( self._context_trim_config.enabled @@ -1206,9 +1143,12 @@ async def _tail(): "[AOAI_CTX_TRIM] sleeping %ss before retry", round(trim_delay, 1) ) await asyncio.sleep(trim_delay) - return await _retry_call( + response = await _retry_call( lambda: parent_inner( messages=trimmed, options=options, stream=False, **kwargs ), config=self._retry_config, ) + # Emit token usage from the retried (context-trimmed) response too. + _emit_usage_from_response(response) + return response diff --git a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py index 6470c4c9..3fb96b37 100644 --- a/src/processor/src/libs/agent_framework/groupchat_orchestrator.py +++ b/src/processor/src/libs/agent_framework/groupchat_orchestrator.py @@ -35,7 +35,13 @@ from mem0 import AsyncMemory from pydantic import AliasChoices, BaseModel, Field, ValidationError -from utils.token_usage_tracker import TokenUsageTracker, extract_usage_from_response, _parse_usage_object +from utils.token_usage_tracker import ( + TokenUsageTracker, + extract_usage_from_response, + _parse_usage_object, + set_active_tracker, + set_token_context, +) logger = logging.getLogger(__name__) @@ -522,7 +528,14 @@ async def run_stream( self._tool_call_recorded.clear() self._tool_call_index.clear() self._streaming_captured_usage = False - self._conversation: list[ChatMessage] = [] # Track conversation during workflow + self._conversation: list[Message] = [] # Track conversation during workflow + + # Register this orchestrator's tracker as the active one so the chat + # client retry wrapper can emit token usage from the raw LLM responses + # (the streaming events / final conversation never surface usage Content). + if self.token_usage_tracker is not None: + set_active_tracker(self.token_usage_tracker) + set_token_context(step_name=self.name) try: # Ensure initialized @@ -809,6 +822,10 @@ async def _handle_agent_update( # twice on parse failure), the executor_id is identical but # response_id differs per invocation. response_id = getattr(event, "response_id", None) + # Update the active agent label so token usage recorded by the chat + # client retry wrapper is attributed to the right agent. + if agent_name: + set_token_context(agent_name=agent_name) await self._start_agent_if_needed( agent_name, stream_callback, callback, response_id=response_id ) @@ -1160,7 +1177,7 @@ def _backfill_tool_usage_from_conversation( continue def _backfill_token_usage_from_conversation( - self, conversation: list[ChatMessage] + self, conversation: list[Message] ) -> None: """Extract token usage from the final conversation messages. diff --git a/src/processor/src/utils/token_usage_tracker.py b/src/processor/src/utils/token_usage_tracker.py index 0799058a..2bffd756 100644 --- a/src/processor/src/utils/token_usage_tracker.py +++ b/src/processor/src/utils/token_usage_tracker.py @@ -14,6 +14,7 @@ """ from __future__ import annotations +import contextvars import logging import threading from dataclasses import dataclass @@ -295,3 +296,106 @@ def _parse_usage_object(usage: Any) -> TokenUsageRecord | None: output_tokens=result.output_tokens, total_tokens=result.total_tokens, ) + + +# --------------------------------------------------------------------------- +# Active-tracker context plumbing +# +# Token usage is reliably extracted deep inside the chat client retry wrapper +# (``azure_openai_response_retry``), which has the raw LLM response with +# ``usage_details``. That layer, however, has no reference to the per-workflow +# ``TokenUsageTracker`` (which owns process/user identity and the App Insights +# emitter). We bridge the two with a context-local + module-global reference to +# the currently active tracker, plus the current agent/step labels. The chat +# client calls :func:`record_usage_from_context` so every LLM call's tokens are +# emitted as ``LLM_*`` custom events even though the orchestrator's streaming +# events and final conversation never surface usage Content. +# +# A ContextVar is used so the value propagates to asyncio tasks created after it +# is set; a module-level global is kept as a fallback because the processor +# handles a single workflow at a time per process (TokenUsageTracker is a +# per-workflow app_context singleton). +# --------------------------------------------------------------------------- + +_current_tracker_ctx: contextvars.ContextVar[TokenUsageTracker | None] = ( + contextvars.ContextVar("token_usage_current_tracker", default=None) +) +_current_agent_ctx: contextvars.ContextVar[str] = contextvars.ContextVar( + "token_usage_current_agent", default="" +) +_current_step_ctx: contextvars.ContextVar[str] = contextvars.ContextVar( + "token_usage_current_step", default="" +) + +# Module-level fallback (single active workflow per process). +_active_tracker: TokenUsageTracker | None = None + + +def set_active_tracker(tracker: TokenUsageTracker | None) -> None: + """Mark ``tracker`` as the active tracker for token emission from the chat client.""" + global _active_tracker + _active_tracker = tracker + try: + _current_tracker_ctx.set(tracker) + except Exception: # pragma: no cover - contextvar set is effectively infallible + pass + + +def clear_active_tracker() -> None: + """Clear the active tracker reference (call when a workflow finishes).""" + set_active_tracker(None) + + +def set_token_context( + *, agent_name: str | None = None, step_name: str | None = None +) -> None: + """Update the current agent/step labels used when recording from context.""" + if agent_name is not None: + _current_agent_ctx.set(agent_name) + if step_name is not None: + _current_step_ctx.set(step_name) + + +def get_active_tracker() -> TokenUsageTracker | None: + """Return the active tracker (ContextVar first, module-global fallback).""" + tracker = _current_tracker_ctx.get() + if tracker is not None: + return tracker + return _active_tracker + + +def record_usage_from_context( + *, + input_tokens: int, + output_tokens: int, + total_tokens: int, + model_deployment_name: str = "", + agent_name: str = "", + step_name: str = "", +) -> bool: + """Record a single LLM call's token usage against the active tracker. + + Resolves the tracker and agent/step labels from context when not provided. + Returns ``True`` if a tracker was available and the usage was recorded (which + also emits per-call ``LLM_*`` App Insights events), ``False`` otherwise. + """ + tracker = get_active_tracker() + if tracker is None: + return False + + agent = agent_name or _current_agent_ctx.get("") or "" + step = step_name or _current_step_ctx.get("") or "" + try: + tracker.record( + input_tokens=int(input_tokens), + output_tokens=int(output_tokens), + total_tokens=int(total_tokens), + agent_name=agent, + step_name=step, + model_deployment_name=model_deployment_name, + ) + return True + except Exception: + logger.exception("[TOKEN] record_usage_from_context failed") + return False + From cddc99a02d60835c322932017bc80bce31ed8f9b Mon Sep 17 00:00:00 2001 From: llPriyanka <40491221+llPriyanka@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:26:59 +0000 Subject: [PATCH 50/50] updated missing u --- src/processor/src/utils/token_usage_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/processor/src/utils/token_usage_tracker.py b/src/processor/src/utils/token_usage_tracker.py index 2bffd756..853a1a28 100644 --- a/src/processor/src/utils/token_usage_tracker.py +++ b/src/processor/src/utils/token_usage_tracker.py @@ -8,7 +8,7 @@ - Per user/process - Per model deployment -ses the cross-accelerator ``llm_token_telemetry`` module for extraction +Uses the cross-accelerator ``llm_token_telemetry`` module for extraction and emission, keeping this tracker as a thin orchestration-specific layer that adds thread-safe aggregation and per-step tracking. """